在上一篇中,蘑菇君记录了自己封装JDBC异常的骚操作。这一次咱们来看看Spring是如何优雅的封装的。

从哪看起呢?这里不得不提一下蘑菇君看源码的思路:

  • 第一步,打开IDE,打开Spring源码
  • 第二步,打开手机,刷刷抖音,看看NBA新闻,想一想中午吃什么
  • 第三步,打开微信,在王者荣耀开黑群里吼一声,“开黑啦!!!1等4,在线等,急!”
  • 第四步,到晚上1点了,关了IDE,洗洗睡了。今天又是爱学习的一天,明天要继续早睡早起,好好加油!

咳咳,上面是错误的打开方式。Spring的源码确实写得很优雅巧妙,然而代码量很大,功能太复杂。真的要直接从源码入手,效率比较低,还容易迷失方向。

那从哪里入手呢?想一想,咱们买个冰箱,要知道冰箱怎么用,当然是先看使用说明书啦!(难不成把冰箱拆开看嘛o(´^`)o)。这里不得不说,Spring的文档写的贼好,十分全面,还详略得当。

Spring官方文档

话音未落,蘑菇君就打开了Spring官网,找到了Spring JDBC异常处理相关的文档:
(找到Spring Framework -> 切换到LearnTab -> 打开Reference Doc)

文档里说明了异常封装和转换是怎么工作的,怎么扩展。先简单理解一下:

SQLExceptionTranslator

SQLExceptionTranslator是一个接口,用于 SQLExceptionsDataAccessException 之间的转换。
咦惹,这看着似曾相识啊。在上一篇中,蘑菇君自定义了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方法是一样的。额外多出来的两个参数tasksql只是为了log的时候能输出更详细的数据。

接下来看一看它的具体实现类:

AbstractFallbackSQLExceptionTranslator一看名字就知道是抽象类。抽象类的作用无非两种:

  1. 提取出子类要用到的通用的方法,避免冗余
  2. 对接口提供通用的扩展功能

再看这个抽象类的名字,不赌五毛钱都知道是要提供兜底策略。这不就跟蘑菇君自定义的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;@Overridepublic 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 {// // 我的备胎是 SQLExceptionSubclassTranslatorpublic SQLErrorCodeSQLExceptionTranslator() {setFallbackTranslator(new SQLExceptionSubclassTranslator());}
}
public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLExceptionTranslator {// 我的备胎是 SQLStateSQLExceptionTranslatorpublic 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 errorBAD_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 {@Overridepublic 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是在哪里用到,又是怎么创建的呢?

蘑菇君有两种方式来找到线索:

  1. 自顶向下:从Spring JDBC执行sql语句的地方找起。执行sql语句总要抛异常的,这异常肯定是要通过translator转换滴。
  2. 从低往上:找到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 translatorexceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);} else {// 否则就创建一个 sql state translatorexceptionTranslator = new SQLStateSQLExceptionTranslator();}this.exceptionTranslator = exceptionTranslator;}return exceptionTranslator;}}
}// 也可以通过这个方法,手动给jdbc template设置一个translatorpublic void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {this.exceptionTranslator = exceptionTranslator;}

可以看到,创建translator的地方在JdbcTemplate的父类JdbcAccessor中,会根据数据源的信息去创建SQLErrorCodeSQLExceptionTranslator。同时,咱们也可以调用set方法手动设置一个translator。

总结

终于写完了,感觉过去了一个世纪。(T▽T)

简单总结一下Spring JDBC异常封装:

  1. 备胎机制。其实是一种责任链模式,将各种translator组合链接在一起,结构清晰优雅,具有很强的容错性。(备胎都比一般人做得好,不服不行~)
  2. 扩展性强。无论是自定义类型转换器,还是增加新的异常类型,都只需实现已有接口,增加新的配置文件即可。对原有框架毫无侵入,留给用户的扩展入口很多,实现起来也很简单。

不得不承认,Spring牛批,比上一篇蘑菇君自己写的强多了╮(╯﹏╰)╭

参考

Spring JDBC SQLExceptionTranslator 官方文档

题外话

我是蘑菇君,大家好,我是猪(T_T)

Spring JDBC的优雅设计 - 异常封装(下)相关推荐

  1. Spring jdbc 对象Mapper的简单封装

    一般查询实体的时候,都需要这么使用/**      * 根据id查询      *       * @return      */     public Emp queryEmpById(Intege ...

  2. 使用Spring JDBC进行数据访问 (JdbcTemplate/NamedParameterJdbcTemplate/SimpleJdbcTemplate/SimpleJdbcCall/Stor)

    http://www.cnblogs.com/webcc/archive/2012/04/11/2442680.html 使用Spring JDBC进行数据访问 11.1. 简介 Spring JDB ...

  3. Spring——DAO层、Spring JDBC、Spring事务控制

    目录 一.Spring对持久层技术支持 1.Spring支持的持久层技术 2.Spring JDBC 2.1. JDBCTemplate类 2.2.Spring JDBC CRUD操作 2.3.Spr ...

  4. Spring JDBC-使用Spring JDBC访问数据库

    概述 使用Spring JDBC 基本的数据操作 更改数据 返回数据库表的自增主键值 批量更改数据 查询数据 使用RowCallbackHandler处理结果集 使用RowMapperT处理结果集 R ...

  5. Spring-Spring MVC + Spring JDBC + Spring Transaction + Maven 构建web登录模块

    概述 功能简介 环境准备 构建工具Maven 数据库脚本Oracle 建立工程 类包及Spring配置文件规划 持久层 建立领域对象 用户领域对象 登录日志领域对象 UserDao LoginLogD ...

  6. Spring JDBC与事务管理

    文章摘要 一.Spring JDBC 1.1.Spring JDBC的使用步骤: 二.jdbcTemplate对象 2.1.查询数据的核心方法 2.2.写入数据的核心方法 三.Spring 编程式事务 ...

  7. Spring Boot 如何优雅的校验参数?

    今天介绍一下 Spring Boot 如何优雅的整合JSR-303进行参数校验,说到参数校验可能都用过,但网上的教程大多是简单的介绍,所以我们今天详细看来一下 . 什么是 JSR-303? JSR-3 ...

  8. 肝!Spring JDBC持久化层框架“全家桶”教程!

    目录 写在前面 一.什么是JdbcTemplate? 二.JdbcTemplate框架搭建 1.导入所需jar包 2.配置JDBC数据源 (1).直接在中配置数据源 (2).引入外部配置文件 3.配置 ...

  9. Spring JDBC详解

    <Spring JDBC详解> 本文旨在讲述Spring JDBC模块的用法.Spring JDBC模块是Spring框架的基础模块之一. 一.概述 在Spring JDBC模块中,所有的 ...

最新文章

  1. 解决linux下中文文件名显示乱码问题
  2. Static Text控件响应函数方法
  3. 小巧数据库 Derby 使用攻略
  4. JS合并数组的几种方法及优劣比较
  5. Linux安装包deb和rpm格式,deb格式和rpm格式是什么?_网站服务器运行维护
  6. boost::process::cmd相关的测试程序
  7. Python + wordcloud + jieba 十分钟学会用任意中文文本生成词云
  8. wordpress使用retro方案出现413 Request Entity Too Large(Activate还是有问题)
  9. c语言找出最大值和最小值并按降序排输出,大学一年级下学期C语言程序设计实验报告答案 完整版...
  10. python计算结果传给spark_Spark入门:流计算简介(Python版)
  11. Oracle的SQL语法提示30例,INDEX_JOIN,ORDERED,USE_NL,LEADING
  12. Linux开发_生成executable (application/x-executable)可执行程序
  13. HTML Button.onclick事件汇总
  14. 创投“黑帮”,必须的
  15. 【转载】使用Winrar对压缩文件进行加密,并且给定解压密码
  16. jfreechart-x轴刻度倾斜45度
  17. Linux安装R相关包出现icudt error
  18. Android设置iptable实现外网访问
  19. 评点SAP HR功能及人力资源管理软件
  20. 爱笑程序员-笑话10则

热门文章

  1. 登录这个轮子,你还在造?
  2. pb在win10中控制是否使用拼音输入法
  3. fzu-1607-Greedy division
  4. 电脑diy配件产品导购,装机硬件资讯
  5. 最详细的【微信小程序+阿里云Web服务】开发部署指引(四):搭建服务端数据库
  6. 解决eclipse控制台不能正常输入问题的心路历程
  7. 网络安全应急演练学习笔记第一篇之总则、分类及方法、组织机构
  8. 人的心情就像四季一样!
  9. Linux服务器配置 (转载)
  10. 使用STWI056WT-01串口屏充电桩项目