Spring系列之JDBC对不同数据库异常如何抽象的?

是这样的,想请说下,Spring系列之JDBC对不同数据库异常如何抽象的?
最新回答
九命猫

2024-10-13 09:03:16

前言

使用Spring-Jdbc的情况下,在有些场景中,我们需要根据数据库报的异常类型的不同,来编写我们的业务代码。比如说,我们有这样一段逻辑,如果我们新插入的记录,存在唯一约束冲突,就会返回给客户端描述:记录已存在,请勿重复操作代码一般是这么写的:

@ResourceprivateJdbcTemplatejdbcTemplate;publicStringtestAdd(){try{jdbcTemplate.execute("INSERTINTOuser_info(user_id,user_name,email,nick_name,status,address)VALUES(80002,'张三丰','xxx@126.com','张真人',1,'武当山');");return"OK";}catch(DuplicateKeyExceptione){return"记录已存在,请勿重复操作";}}

测试一下:

如上图提示,并且无论什么更换什么数据库(Spring-Jdbc支持的),代码都不用改动

那么Spring-Jdbc是在使用不同数据库时,Spring如何帮我们实现对异常的抽象的呢?

代码实现

我们来正向看下代码:首先入口JdbcTemplate.execute方法:

publicvoidexecute(finalStringsql)throwsDataAccessException{if(this.logger.isDebugEnabled()){this.logger.debug("ExecutingSQLstatement["+sql+"]");}...//实际执行入口this.execute(newExecuteStatementCallback(),true);}

内部方法execute

@Nullableprivate<T>Texecute(StatementCallback<T>action,booleancloseResources)throwsDataAccessException{Assert.notNull(action,"Callbackobjectmustnotbenull");Connectioncon=DataSourceUtils.getConnection(this.obtainDataSource());Statementstmt=null;Objectvar12;try{stmt=con.createStatement();this.applyStatementSettings(stmt);Tresult=action.doInStatement(stmt);this.handleWarnings(stmt);var12=result;}catch(SQLExceptionvar10){Stringsql=getSql(action);JdbcUtils.closeStatement(stmt);stmt=null;DataSourceUtils.releaseConnection(con,this.getDataSource());con=null;//SQL出现异常后,在这里进行异常转换throwthis.translateException("StatementCallback",sql,var10);}finally{if(closeResources){JdbcUtils.closeStatement(stmt);DataSourceUtils.releaseConnection(con,this.getDataSource());}}returnvar12;}

异常转换方法translateException

protectedDataAccessExceptiontranslateException(Stringtask,@NullableStringsql,SQLExceptionex){//获取异常转换器,然后根据数据库返回码相关信息执行转换操作//转换不成功,也有兜底异常UncategorizedSQLExceptionDataAccessExceptiondae=this.getExceptionTranslator().translate(task,sql,ex);return(DataAccessException)(dae!=null?dae:newUncategorizedSQLException(task,sql,ex));}

获取转换器方法getExceptionTranslator

publicSQLExceptionTranslatorgetExceptionTranslator(){//获取转换器属性,如果为空,则生成一个SQLExceptionTranslatorexceptionTranslator=this.exceptionTranslator;if(exceptionTranslator!=null){returnexceptionTranslator;}else{synchronized(this){SQLExceptionTranslatorexceptionTranslator=this.exceptionTranslator;if(exceptionTranslator==null){DataSourcedataSource=this.getDataSource();//shouldIgnoreXml是一个标记,就是不通过xml加载bean,默认falseif(shouldIgnoreXml){exceptionTranslator=newSQLExceptionSubclassTranslator();}elseif(dataSource!=null){//如果DataSource不为空,则生成转换器SQLErrorCodeSQLExceptionTranslatorexceptionTranslator=newSQLErrorCodeSQLExceptionTranslator(dataSource);}else{//其他情况,生成SQLStateSQLExceptionTranslator转换器exceptionTranslator=newSQLStateSQLExceptionTranslator();}this.exceptionTranslator=(SQLExceptionTranslator)exceptionTranslator;}return(SQLExceptionTranslator)exceptionTranslator;}}}

转换方法:因为默认的转换器是SQLErrorCodeSQLExceptionTranslator,所以这里调用SQLErrorCodeSQLExceptionTranslator的doTranslate方法

类图调用关系如上,实际先调用的是AbstractFallbackSQLExceptionTranslator.translate的方法

@NullablepublicDataAccessExceptiontranslate(Stringtask,@NullableStringsql,SQLExceptionex){Assert.notNull(ex,"CannottranslateanullSQLException");//这里才真正调用SQLErrorCodeSQLExceptionTranslator.doTranslate方法DataAccessExceptiondae=this.doTranslate(task,sql,ex);if(dae!=null){returndae;}else{//如果没有找到响应的异常,则调用其他转换器,输入递归调用,这里后面说SQLExceptionTranslatorfallback=this.getFallbackTranslator();returnfallback!=null?fallback.translate(task,sql,ex):null;}}

实际转换类SQLErrorCodeSQLExceptionTranslator的方法:

//这里省略了一些无关代码,只保留了核心代码//先获取SQLErrorCodes集合,在根据返回的SQLException中获取的ErrorCode进行匹配,根据匹配结果进行返回响应的异常protectedDataAccessExceptiondoTranslate(Stringtask,@NullableStringsql,SQLExceptionex){....SQLErrorCodessqlErrorCodes=this.getSqlErrorCodes();StringerrorCode=Integer.toString(ex.getErrorCode());...if(Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(),errorCode)>=0){this.logTranslation(task,sql,sqlEx,false);returnnewDuplicateKeyException(this.buildMessage(task,sql,sqlEx),sqlEx);}...returnnull;}

上面的SQLErrorCodes是一个错误码集合,但是不是全部数据库的所有错误码集合,而是只取了相应数据库的错误码集合,怎么保证获取的是当前使用的数据库的错误码,而不是其他数据库的错误码呢?当然Spring为我们实现了,在SQLErrorCodeSQLExceptionTranslator中:

publicclassSQLErrorCodeSQLExceptionTranslatorextendsAbstractFallbackSQLExceptionTranslator{privateSingletonSupplier<SQLErrorCodes>sqlErrorCodes;//默认构造方法,设置了如果转换失败,下一个转换器是SQLExceptionSubclassTranslatorpublicSQLErrorCodeSQLExceptionTranslator(){this.setFallbackTranslator(newSQLExceptionSubclassTranslator());}//前面生成转换器的时候,exceptionTranslator=newSQLErrorCodeSQLExceptionTranslator(dataSource);//使用的是本构造方法,传入了DataSource,其中有数据库厂商信息,本文中是MYSQLpublicSQLErrorCodeSQLExceptionTranslator(DataSourcedataSource){this();this.setDataSource(dataSource);}//从错误码工厂SQLErrorCodesFactory里,获取和数据源对应的厂商的所有错误码publicvoidsetDataSource(DataSourcedataSource){this.sqlErrorCodes=SingletonSupplier.of(()->{returnSQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource);});this.sqlErrorCodes.get();}}

错误码工厂SQLErrorCodesFactory的resolveErrorCodes方法:

//既然是工厂,里面肯定有各种数据库的错误码,本文中使用的是MYSQL,我们看一下实现逻辑@NullablepublicSQLErrorCodesresolveErrorCodes(DataSourcedataSource){Assert.notNull(dataSource,"DataSourcemustnotbenull");if(logger.isDebugEnabled()){logger.debug("LookingupdefaultSQLErrorCodesforDataSource["+this.identify(dataSource)+"]");}//从缓存中拿MYSQL对应的SQLErrorCodesSQLErrorCodessec=(SQLErrorCodes)this.dataSourceCache.get(dataSource);if(sec==null){synchronized(this.dataSourceCache){sec=(SQLErrorCodes)this.dataSourceCache.get(dataSource);if(sec==null){try{Stringname=(String)JdbcUtils.extractDatabaseMetaData(dataSource,DatabaseMetaData::getDatabaseProductName);if(StringUtils.hasLength(name)){SQLErrorCodesvar10000=this.registerDatabase(dataSource,name);returnvar10000;}}catch(MetaDataAccessExceptionvar6){logger.warn("Errorwhileextractingdatabasename",var6);}returnnull;}}}if(logger.isDebugEnabled()){logger.debug("SQLErrorCodesfoundincacheforDataSource["+this.identify(dataSource)+"]");}returnsec;}

缓存dataSourceCache如何生成的?

publicSQLErrorCodesregisterDatabase(DataSourcedataSource,StringdatabaseName){//根据数据库类型名称(这里是MySQL),获取错误码列表SQLErrorCodessec=this.getErrorCodes(databaseName);if(logger.isDebugEnabled()){logger.debug("CachingSQLerrorcodesforDataSource["+this.identify(dataSource)+"]:databaseproductnameis'"+databaseName+"'");}this.dataSourceCache.put(dataSource,sec);returnsec;}publicSQLErrorCodesgetErrorCodes(StringdatabaseName){Assert.notNull(databaseName,"Databaseproductnamemustnotbenull");//从errorCodesMap根据key=MYSQL获取SQLErrorCodesSQLErrorCodessec=(SQLErrorCodes)this.errorCodesMap.get(databaseName);if(sec==null){Iteratorvar3=this.errorCodesMap.values().iterator();while(var3.hasNext()){SQLErrorCodescandidate=(SQLErrorCodes)var3.next();if(PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(),databaseName)){sec=candidate;break;}}}if(sec!=null){this.checkCustomTranslatorRegistry(databaseName,sec);if(logger.isDebugEnabled()){logger.debug("SQLerrorcodesfor'"+databaseName+"'found");}returnsec;}else{if(logger.isDebugEnabled()){logger.debug("SQLerrorcodesfor'"+databaseName+"'notfound");}returnnewSQLErrorCodes();}}//SQLErrorCodesFactory构造方法中,生成的errorCodesMap,map的内容来自org/springframework/jdbc/support/sql-error-codes.xml文件protectedSQLErrorCodesFactory(){MaperrorCodes;try{DefaultListableBeanFactorylbf=newDefaultListableBeanFactory();lbf.setBeanClassLoader(this.getClass().getClassLoader());XmlBeanDefinitionReaderbdr=newXmlBeanDefinitionReader(lbf);Resourceresource=this.loadResource("org/springframework/jdbc/support/sql-error-codes.xml");if(resource!=null&&resource.exists()){bdr.loadBeanDefinitions(resource);}else{logger.info("Defaultsql-error-codes.xmlnotfound(shouldbeincludedinspring-jdbcjar)");}resource=this.loadResource("sql-error-codes.xml");if(resource!=null&&resource.exists()){bdr.loadBeanDefinitions(resource);logger.debug("Foundcustomsql-error-codes.xmlfileattherootoftheclasspath");}errorCodes=lbf.getBeansOfType(SQLErrorCodes.class,true,false);if(logger.isTraceEnabled()){logger.trace("SQLErrorCodesloaded:"+errorCodes.keySet());}}catch(BeansExceptionvar5){logger.warn("ErrorloadingSQLerrorcodesfromconfigfile",var5);errorCodes=Collections.emptyMap();}this.errorCodesMap=errorCodes;}

sql-error-codes.xml文件中配置了各个数据库的主要的错误码这里列举了MYSQL部分,当然还有其他部分,我们可以看到唯一性约束错误码是1062,就可以翻译成DuplicateKeyException异常了

publicvoidexecute(finalStringsql)throwsDataAccessException{if(this.logger.isDebugEnabled()){this.logger.debug("ExecutingSQLstatement["+sql+"]");}...//实际执行入口this.execute(newExecuteStatementCallback(),true);}0

你已经看到,比如上面的错误码值列举了一部分,如果出现了一个不在其中的错误码肯定是匹配不到,Spring当然能想到这种情况了

publicvoidexecute(finalStringsql)throwsDataAccessException{if(this.logger.isDebugEnabled()){this.logger.debug("ExecutingSQLstatement["+sql+"]");}...//实际执行入口this.execute(newExecuteStatementCallback(),true);}1

SQLErrorCodeSQLExceptionTranslator的后置转换器是什么?

publicvoidexecute(finalStringsql)throwsDataAccessException{if(this.logger.isDebugEnabled()){this.logger.debug("ExecutingSQLstatement["+sql+"]");}...//实际执行入口this.execute(newExecuteStatementCallback(),true);}2

SQLExceptionSubclassTranslator的转换方法逻辑如下:

publicvoidexecute(finalStringsql)throwsDataAccessException{if(this.logger.isDebugEnabled()){this.logger.debug("ExecutingSQLstatement["+sql+"]");}...//实际执行入口this.execute(newExecuteStatementCallback(),true);}3

SQLStateSQLExceptionTranslator的转换方法:

publicvoidexecute(finalStringsql)throwsDataAccessException{if(this.logger.isDebugEnabled()){this.logger.debug("ExecutingSQLstatement["+sql+"]");}...//实际执行入口t