Spring JDBC的优雅设计 - 异常封装(下)
Posted 蘑菇君520
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring JDBC的优雅设计 - 异常封装(下)相关的知识,希望对你有一定的参考价值。
在上一篇中,蘑菇君记录了自己封装JDBC异常的骚操作。这一次咱们来看看Spring是如何优雅的封装的。
从哪看起呢?这里不得不提一下蘑菇君看源码的思路:
- 第一步,打开IDE,打开Spring源码
- 第二步,打开手机,刷刷抖音,看看NBA新闻,想一想中午吃什么
- 第三步,打开微信,在王者荣耀开黑群里吼一声,“开黑啦!!!1等4,在线等,急!”
- 第四步,到晚上1点了,关了IDE,洗洗睡了。今天又是爱学习的一天,明天要继续早睡早起,好好加油!
咳咳,上面是错误的打开方式。Spring的源码确实写得很优雅巧妙,然而代码量很大,功能太复杂。真的要直接从源码入手,效率比较低,还容易迷失方向。
那从哪里入手呢?想一想,咱们买个冰箱,要知道冰箱怎么用,当然是先看使用说明书啦!(难不成把冰箱拆开看嘛o(´^`)o)。这里不得不说,Spring的文档写的贼好,十分全面,还详略得当。
Spring官方文档
话音未落,蘑菇君就打开了Spring官网,找到了Spring JDBC异常处理相关的文档:
(找到Spring Framework -> 切换到LearnTab -> 打开Reference Doc)
文档里说明了异常封装和转换是怎么工作的,怎么扩展。先简单理解一下:
SQLExceptionTranslator
SQLExceptionTranslator
是一个接口,用于 SQLExceptions
和 DataAccessException
之间的转换。
咦惹,这看着似曾相识啊。在上一篇中,蘑菇君自定义了MoguJunSQLException
,跟DataAccessException
类似。还自定义了SQLExceptionParser
接口用于转换,跟SQLExceptionTranslator
类似。(好,Spring抄袭蘑菇君的思路实锤了~(ノ゚∀゚)ノ )
SQLErrorCodes
SQLErrorCodes
保存了所有错误码信息,并且该类由SQLErrorCodesFactory
创建和管理。一个SQLErrorCodes
实例对应着一组数据库的错误码和异常映射。
SQLErrorCodesFactory
SQLErrorCodesFactory
用来定义错误码以及自定义异常转换机制。它会查询 classpath 下的sql-error-codes.xml 文件,根据数据库名去匹配到某个SQLErrorCodes
实例。
SQLErrorCodeSQLExceptionTranslator
是SQLExceptionTranslator
接口的默认实现类,用数据库厂商的错误码去转换异常。可以通过继承该类实现自己的转换逻辑。
源码分析
接下来我们来详细看看上面文档中提到的类~
备胎机制
首先是最核心的接口SQLExceptionTranslator
:
public interface SQLExceptionTranslator
DataAccessException translate(String task, @Nullable String sql, SQLException ex);
里面就一个方法translate
,跟上一篇中提到的SQLExceptionParser
里的parse
方法是一样的。额外多出来的两个参数task
和sql
只是为了log的时候能输出更详细的数据。
接下来看一看它的具体实现类:
AbstractFallbackSQLExceptionTranslator
一看名字就知道是抽象类。抽象类的作用无非两种:
- 提取出子类要用到的通用的方法,避免冗余
- 对接口提供通用的扩展功能
再看这个抽象类的名字,不赌五毛钱都知道是要提供兜底策略。这不就跟蘑菇君自定义的DefaultExceptionParser
达到的效果一样么?回顾一下蘑菇君的兜底实现方式:
public class SQLExceptionParserFactory
public SQLExceptionParser create(String databaseVendor)
switch (databaseVendor)
case "mysql":
return new MySQLExceptionParser();
case "Oracle":
return new OracleExceptionParser();
// 省略其他数据库
return new DefaultSQLExceptionParser();
这个兜底比较僵硬,只要匹配不到转换类,都会固定的去调用DefaultSQLExceptionParser
的转换逻辑。简单来说,DefaultSQLExceptionParser
就是所有人心目中的万年不变的唯一备胎。(不知道该哭还是该笑(•́へ•́))
再来看看Spring的备胎机制:
public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExceptionTranslator
// 备胎在此
private SQLExceptionTranslator fallbackTranslator;
@Override
public DataAccessException translate(String task, String sql, SQLException ex)
DataAccessException dae = doTranslate(task, sql, ex);
if (dae != null)
// 如果自己能转换成功,那就没备胎什么事儿了~
return dae;
// 如果自己转换不了,又有备胎,那就让备胎去干咯~
if (fallbackTranslator!= null)
dae = fallback.translate(task, sql, ex);
if (dae != null)
return dae;
// 如果备胎都不行,那就无能为力了,抛异常呗!
return new UncategorizedSQLException(task, sql, ex);
// 给子类实现的方法
protected abstract DataAccessException doTranslate(String task, String sql, SQLException ex);
备胎机制在translate
方法里实现了,同时又暴露了doTranslate
抽象方法给子类去实现自己的转换逻辑。
让咱们看看究竟谁是谁的备胎~(贵圈真乱…)
public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator
// // 我的备胎是 SQLExceptionSubclassTranslator
public SQLErrorCodeSQLExceptionTranslator()
setFallbackTranslator(new SQLExceptionSubclassTranslator());
public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLExceptionTranslator
// 我的备胎是 SQLStateSQLExceptionTranslator
public SQLExceptionSubclassTranslator()
setFallbackTranslator(new SQLStateSQLExceptionTranslator());
public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator
// 咦,我的备胎呢?憋看了,你就是终极备胎!!!
看到这儿,我不禁感叹一声,Spring真是太渣了。这备胎一个还不够,备胎还有备胎,形成了一个备胎链:
SQLErrorCodeSQLExceptionTranslator <--- SQLExceptionSubclassTranslator <-- SQLStateSQLExceptionTranslator
内置的Translator实现
SQLStateSQLExceptionTranslator
SQLStateSQLExceptionTranslator
是终极备胎,根据SQLState的错误类别class,来返回异常子类。这是最宽泛的一种错误。
public class SQLStateSQLExceptionTranslator
private static final Set<String> BAD_SQL_GRAMMAR_CODES = new HashSet<>(8);
private static final Set<String> DATA_INTEGRITY_VIOLATION_CODES = new HashSet<>(8);
...
static
// SQL标准里定义的错误类别码
BAD_SQL_GRAMMAR_CODES.add("07"); // Dynamic SQL error
BAD_SQL_GRAMMAR_CODES.add("21"); // Cardinality violation
...
// 根据错误类别码去返回异常类,比较粗略宽泛
protected DataAccessException doTranslate(String task, String sql, SQLException ex)
String classCode = sqlState.substring(0, 2);
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);
SQLExceptionSubclassTranslator
SQLExceptionSubclassTranslator
会根据JDBC 4中提供的异常子类去封装,并返回Spring的异常子类。但是有些driver可能并没有用jdbc 4的版本,就匹配不到咯,就需要备胎了。
public class SQLExceptionSubclassTranslator
// 根据 JDBC 4 中提供的异常子类去封装自己的异常类
protected DataAccessException doTranslate(String task, String sql, SQLException ex)
if (ex instanceof SQLTransientException)
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);
... 省略
SQLErrorCodeSQLExceptionTranslator
SQLErrorCodeSQLExceptionTranslator
是最精确的,因为error code是数据库厂商提供的具体的错误信息。代码比较长,就不贴了。过程可以分为如下几步:
(1) 调用customTranslate()
方法拿到一个Translator,进行转换。
customTranslate
默认返回null,留着给子类实现的。也就是说,这里是个扩展点,使用者可以继承SQLErrorCodeSQLExceptionTranslator
类来实现自己的转换逻辑。
(2)拿到SQLErrorCodes
类里的customTranslations
,进行转换。
这里又是一个扩展点。咱们也可以在SQLErrorCodes
里指定一个转换类,实现自定义的转换处理。
(3)解析error code。上一篇里提到过,咱们得让用户可以自定义error code和异常类的映射,才能适应各种情况。在解析error code的逻辑中,SQLErrorCodeSQLExceptionTranslator
先拿SQLErrorCodes
里的customTranslations
解析error code,如果没能解析到自定义的异常,就去SQLErrorCodes
里内置的error code和异常类的映射里找。
(4)要是仍然没能转换出具体的异常,那就只好交给备胎咯。
错误码异常映射
咱们来看看SQLErrorCodes
类,官方文档里提到过,一个SQLErrorCodes
实例对应着一组数据库的错误码和异常映射。
public class SQLErrorCodes
@Nullable // 可以对应多个数据库,可能一个数据库厂商有多个数据库产品,但是错误码是一样的
private String[] databaseProductNames;
// 内置了10种错误码类型数组,也对应着Spring里内置的十种数据库异常
private String[] badSqlGrammarCodes = new String[0];
private String[] invalidResultSetAccessCodes = new String[0];
private String[] duplicateKeyCodes = new String[0];
// 省略其它7种......
// 自定义的错误码转换数组
private CustomSQLErrorCodesTranslation[] customTranslations;
// 自定义的异常转换类
private SQLExceptionTranslator customSqlExceptionTranslator;
再看看自定义的错误码转换类CustomSQLErrorCodesTranslation
public class CustomSQLErrorCodesTranslation
// 错误码数组,这里可以看出,一个异常可以对应一组错误码
private String[] errorCodes = new String[0];
// 异常类
private Class<?> exceptionClass;
看得出来,SQLErrorCodes
就相当于一个错误码配置项,存放着错误码和异常的映射。那这些映射数据是从哪儿来的呢?根据文档,我们知道SQLErrorCodesFactory
工厂类会根据数据库厂商名字加载出ErrorCodes
对象。这跟上一篇蘑菇君的思路是一样的。
public class SQLErrorCodesFactory
// 配置文件所在路径
public static final String SQL_ERROR_CODE_OVERRIDE_PATH = "sql-error-codes.xml";
public static final String SQL_ERROR_CODE_DEFAULT_PATH =
"org/springframework/jdbc/support/sql-error-codes.xml";
// 保存数据库名 -> SQLErrorCodes的映射
private final Map<String, SQLErrorCodes> errorCodesMap;
// 根据数据源或者数据库名去添加SQLErrorCodes, 保存到map里
public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName)
// 删除
public SQLErrorCodes unregisterDatabase(DataSource dataSource)
// 查找
public SQLErrorCodes getErrorCodes(String databaseName)
SQLErrorCodes sec = this.errorCodesMap.get(databaseName);
private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory();
// 在构造方法里解析配置文件,初始化
protected SQLErrorCodesFactory()
// 加载默认的 SQL error codes.
Resource resource = loadResource(SQL_ERROR_CODE_DEFAULT_PATH);
bdr.loadBeanDefinitions(resource);
// 加载classpath下的自定义的error codes配置,会覆盖默认的 error codes配置
resource = loadResource(SQL_ERROR_CODE_OVERRIDE_PATH);
if (resource != null && resource.exists())
bdr.loadBeanDefinitions(resource);
// 生成ErrorCodes实例
errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false);
...
话不多说,一切都在注释里~(~ ̄▽ ̄)~ …
emmm, 还是多说一句好了。配置文件是以Bean的方式来解析的,说明SQLErrorCode
在配置文件里就是一个Bean。同时,Spring提供了默认的配置文件,我们也可以选择覆盖默认配置。(这特么是多说一句么…<( ̄ ﹌  ̄)> )
来看看默认的配置文件:
<beans>
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>1054,1064,1146</value>
</property>
<property name="duplicateKeyCodes">
<value>1062</value>
</property>
... 省略
</bean>
<bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>900,903,904,917,936,942,17006,6550</value>
</property>
<property name="duplicateKeyCodes">
<value>1</value>
</property>
... 省略
<bean>
... 省略其他常用数据库错误码配置
</beans>
通过Spring的IOC框架,将配置文件里的Bean配置注入并生成了SQLErrorCodes
实例。
如果咱们想配置新的数据库,可以创建新的sql-error-codes.xml
配置文件放在classpath下:
<beans>
<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>42000,42001,42101,42102</value>
</property>
<property name="duplicateKeyCodes">
<value>23001,23505</value>
</property>
// 自定义错误码解析
<property name="customTranslations">
<bean class="org.springframework.jdbc.support.CustomSQLErrorCodesTranslation">
<property name="errorCodes" value="23001,23505" />
<property name="exceptionClass"
value="wang.mogujun.sqlexception.ForeignKeyViolationException" />
</bean>
</property>
// 自定义异常转换类
<property name="customSqlExceptionTranslator">
<bean class="wang.mogujun.sqlexception.MoguJunSQLExceptionTranslator" />
</property>
</bean>
</beans>
举个栗子,搞个自定义的Translator,干点大事儿:
public class MoguJunSQLExceptionTranslator implements SQLExceptionTranslator
@Override
public DataAccessException translate(final String task, final String sql,
final SQLException sqlEx)
log.info(" SQLException with SQL state '" + sqlEx.getSQLState() +
"', error code '" + sqlEx.getErrorCode() + "', message [" + sqlEx.getMessage() + "]" +
(sql != null ? "; SQL was [" + sql + "]": "") + " for task [" + task + "]");
return null;
咳咳,那啥吧,其实没干啥大事儿,甚至没干啥正当的事儿,就是记录异常信息到日志系统里。但是,也证明了Spring这一套机制扩展性很好。今天我能记录log,明天就能删库跑路,后天就能上天( ̄▽ ̄)/
封装使用
转换的过程以及如何扩展咱们已经知道了。那上面的SQLErrorCodeSQLExceptionTranslator
是在哪里用到,又是怎么创建的呢?
蘑菇君有两种方式来找到线索:
- 自顶向下:从Spring JDBC执行sql语句的地方找起。执行sql语句总要抛异常的,这异常肯定是要通过translator转换滴。
- 从低往上:找到
SQLErrorCodeSQLExceptionTranslator
的所有被调用的地方,再一直溯源到它的创建和使用。
这两种方式都行,咱们用第一种试试。Spring JDBC执行sql语句,都是通过JdbcTemplate
类来执行的。
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations
public <T> T execute(StatementCallback<T> action) throws DataAccessException
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try
...
catch (SQLException ex)
// 将SQLException 转换成DataAccessException
throw translateException("StatementCallback", sql, ex);
protected DataAccessException translateException(String task, String sql, SQLException ex)
DataAccessException dae = getExceptionTranslator().translate(task, sql, ex);
return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
咱们打开其中一个execute方法,一直跟下去,就发现了translateException()
方法。这方法就是通过getExceptionTranslator()
拿到translator去转换异常的。继续跟下去:
public abstract class JdbcAccessor implements InitializingBean
private volatile SQLExceptionTranslator exceptionTranslator;
private DataSource dataSource;
public SQLExceptionTranslator getExceptionTranslator()
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator != null)
return exceptionTranslator;
synchronized (this)
exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator == null)
DataSource dataSource = getDataSource();
if (dataSource != null)
// 如果拿到数据源,就用数据源信息去创建error code translator
exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
else
// 否则就创建一个 sql state translator
exceptionTranslator = new SQLStateSQLExceptionTranslator();
this.exceptionTranslator = exceptionTranslator;
return exceptionTranslator;
// 也可以通过这个方法,手动给jdbc template设置一个translator
public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator)
this.exceptionTranslator = exceptionTranslator;
可以看到,创建translator的地方在JdbcTemplate
的父类JdbcAccessor
中,会根据数据源的信息去创建SQLErrorCodeSQLExceptionTranslator
。同时,咱们也可以调用set方法手动设置一个translator。
总结
终于写完了,感觉过去了一个世纪。(T▽T)
简单总结一下Spring JDBC异常封装:
- 备胎机制。其实是一种责任链模式,将各种translator组合链接在一起,结构清晰优雅,具有很强的容错性。(备胎都比一般人做得好,不服不行~)
- 扩展性强。无论是自定义类型转换器,还是增加新的异常类型,都只需实现已有接口,增加新的配置文件即可。对原有框架毫无侵入,留给用户的扩展入口很多,实现起来也很简单。
不得不承认,Spring牛批,比上一篇蘑菇君自己写的强多了╮(╯﹏╰)╭
参考
Spring JDBC SQLExceptionTranslator 官方文档
题外话
我是蘑菇君,大家好,我是猪(T_T)
以上是关于Spring JDBC的优雅设计 - 异常封装(下)的主要内容,如果未能解决你的问题,请参考以下文章