本篇内容介绍了“Spring转换SQLException并埋入扩展点的工作过程”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
我不知道这样理解是否正确:
SQLState 的来源是对 SQL 标准的履约。尽管 SQL 数据库的厂商有很多,只要它们都尊重 SQL 标准和 JDBC 的规范,那么不同厂商在同样的错误上必然返回同样的 SQLState。
vendorCode 很魔幻,它对应的 getter 方法名为 getErrorCode。但字段名里的 vendor 和它的 getter 方法上的注解已经将出卖了它的意义。vendorCode 或者说 errorCode 并不是 SQL 标准的内容,不同数据库厂商可能对同样的错误返回不同的 errorCode。
我本来猜测默认的转换器 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
* <code>SQLException</code> object by setNextException(SQLException ex).
*
* @return the next <code>SQLException</code> object in the chain;
* <code>null</code> if there are none
* @see #setNextException
*/
public SQLException getNextException() {
return (next);
}
/**
* Adds an <code>SQLException</code> object to the end of the chain.
*
* @param ex the new exception that will be added to the end of
* the <code>SQLException</code> 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 数据库的内容:
<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 处理不过来的 SQLException 做一些日志记录,不值得多说了。
第一个扩展点是 protected
的方法,用户可以通过继承后在此方法中添加自己的实现的实现后将自己的实现类注册为 Bean,再结合 Spring 为默认的实现注册 Bean 时的 @ConditionalOnMissingBean
限制,达成了扩展点。
随后的几个扩展点的本质都是为默认实现的 Bean 中留下可注入的成员,用户通过实现特定接口并将其注册为 Bean,结合成员注入将带着自己实现逻辑的 Bean 注入后将自己的私活带入。
“Spring转换SQLException并埋入扩展点的工作过程”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:https://my.oschina.net/u/4249270/blog/5033565