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是一个接口,用于 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;

	@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是在哪里用到,又是怎么创建的呢?

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

  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 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异常封装:

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

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

参考

Spring JDBC SQLExceptionTranslator 官方文档

题外话

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

以上是关于Spring JDBC的优雅设计 - 异常封装(下)的主要内容,如果未能解决你的问题,请参考以下文章

Spring JDBC的优雅设计 - 异常封装(上)

Spring JDBC的优雅设计 - 异常封装(上)

Spring JDBC的优雅设计 - 数据转换

Spring JDBC的优雅设计 - 数据转换

spring的全局自定义异常案例「完美拦截Controller层全部异常」

Java JDBC的优雅设计