詳解Spring不同數(shù)據(jù)庫異常如何抽象的
前言
使用Spring-Jdbc的情況下,在有些場(chǎng)景中,我們需要根據(jù)數(shù)據(jù)庫報(bào)的異常類型的不同,來編寫我們的業(yè)務(wù)代碼。比如說,我們有這樣一段邏輯,如果我們新插入的記錄,存在唯一約束沖突,就會(huì)返回給客戶端描述:記錄已存在,請(qǐng)勿重復(fù)操作
代碼一般是這么寫的:
@Resource private JdbcTemplate jdbcTemplate; public String testAdd(){ try { jdbcTemplate.execute("INSERT INTO user_info (user_id, user_name, email, nick_name, status, address) VALUES (80002, '張三豐', 'xxx@126.com', '張真人', 1, '武當(dāng)山');"); return "OK"; }catch (DuplicateKeyException e){ return "記錄已存在,請(qǐng)勿重復(fù)操作"; } }
測(cè)試一下:
如上圖提示,并且無論什么更換什么數(shù)據(jù)庫(Spring-Jdbc支持的),代碼都不用改動(dòng)
那么Spring-Jdbc是在使用不同數(shù)據(jù)庫時(shí),Spring如何幫我們實(shí)現(xiàn)對(duì)異常的抽象的呢?
代碼實(shí)現(xiàn)
我們來正向看下代碼:
首先入口JdbcTemplate.execute方法:
public void execute(final String sql) throws DataAccessException { if (this.logger.isDebugEnabled()) { this.logger.debug("Executing SQL statement [" + sql + "]"); } ... //實(shí)際執(zhí)行入口,調(diào)用內(nèi)部方法 this.execute(new ExecuteStatementCallback(), true); }
內(nèi)部方法execute
@Nullable private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Connection con = DataSourceUtils.getConnection(this.obtainDataSource()); Statement stmt = null; Object var12; try { ... } catch (SQLException var10) { .... //SQL出現(xiàn)異常后,所有的異常在這里進(jìn)行異常轉(zhuǎn)換 throw this.translateException("StatementCallback", sql, var10); } finally { if (closeResources) { JdbcUtils.closeStatement(stmt); DataSourceUtils.releaseConnection(con, this.getDataSource()); } } return var12; }
異常轉(zhuǎn)換方法translateException
protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) { //獲取異常轉(zhuǎn)換器,然后根據(jù)數(shù)據(jù)庫返回碼相關(guān)信息執(zhí)行轉(zhuǎn)換操作 //轉(zhuǎn)換不成功,也有兜底異常UncategorizedSQLException DataAccessException dae = this.getExceptionTranslator().translate(task, sql, ex); return (DataAccessException)(dae != null ? dae : new UncategorizedSQLException(task, sql, ex)); }
獲取轉(zhuǎn)換器方法getExceptionTranslator
public SQLExceptionTranslator getExceptionTranslator() { //獲取轉(zhuǎn)換器屬性,如果為空,則生成一個(gè) SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator; if (exceptionTranslator != null) { return exceptionTranslator; } else { synchronized(this) { SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator; if (exceptionTranslator == null) { DataSource dataSource = this.getDataSource(); //shouldIgnoreXml是一個(gè)標(biāo)記,就是不通過xml加載bean,默認(rèn)false if (shouldIgnoreXml) { exceptionTranslator = new SQLExceptionSubclassTranslator(); } else if (dataSource != null) { //如果DataSource不為空,則生成轉(zhuǎn)換器SQLErrorCodeSQLExceptionTranslator,一般情況下首先獲取到該轉(zhuǎn)換器 exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); } else { // 其他情況,生成SQLStateSQLExceptionTranslator轉(zhuǎn)換器 exceptionTranslator = new SQLStateSQLExceptionTranslator(); } this.exceptionTranslator = (SQLExceptionTranslator)exceptionTranslator; } return (SQLExceptionTranslator)exceptionTranslator; } } }
轉(zhuǎn)換方法:
因?yàn)槟J(rèn)的轉(zhuǎn)換器是SQLErrorCodeSQLExceptionTranslator,所以這里調(diào)用SQLErrorCodeSQLExceptionTranslator的doTranslate方法
類圖調(diào)用關(guān)系如上,實(shí)際先調(diào)用的是AbstractFallbackSQLExceptionTranslator.translate的方法
@Nullable public DataAccessException translate(String task, @Nullable String sql, SQLException ex) { Assert.notNull(ex, "Cannot translate a null SQLException"); //這里才真正調(diào)用SQLErrorCodeSQLExceptionTranslator.doTranslate方法 DataAccessException dae = this.doTranslate(task, sql, ex); if (dae != null) { return dae; } else { //如果沒有找到響應(yīng)的異常,則調(diào)用其他轉(zhuǎn)換器,輸入遞歸調(diào)用,這里后面說 SQLExceptionTranslator fallback = this.getFallbackTranslator(); return fallback != null ? fallback.translate(task, sql, ex) : null; } }
實(shí)際轉(zhuǎn)換類SQLErrorCodeSQLExceptionTranslator的方法:
//這里省略了一些無關(guān)代碼,只保留了核心代碼 //先獲取SQLErrorCodes集合,在根據(jù)返回的SQLException中獲取的ErrorCode進(jìn)行匹配,根據(jù)匹配結(jié)果進(jìn)行返回響應(yīng)的異常 protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { .... SQLErrorCodes sqlErrorCodes = this.getSqlErrorCodes(); String errorCode = Integer.toString(ex.getErrorCode()); ... //這里用1062唯一性約束沖突,所以走到這里的邏輯,從而返回DuplicateKeyException if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) { this.logTranslation(task, sql, sqlEx, false); return new DuplicateKeyException(this.buildMessage(task, sql, sqlEx), sqlEx); } ... return null; }
上面的SQLErrorCodes是一個(gè)錯(cuò)誤碼集合,但是不是全部數(shù)據(jù)庫的所有錯(cuò)誤碼集合,而是只取了相應(yīng)數(shù)據(jù)庫的錯(cuò)誤碼集合,怎么保證獲取的是當(dāng)前使用的數(shù)據(jù)庫的錯(cuò)誤碼,而不是其他數(shù)據(jù)庫的錯(cuò)誤碼呢?當(dāng)然Spring為我們實(shí)現(xiàn)了,在SQLErrorCodeSQLExceptionTranslator中:
public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { private SingletonSupplier<SQLErrorCodes> sqlErrorCodes; //默認(rèn)構(gòu)造方法,設(shè)置了如果轉(zhuǎn)換失敗,下一個(gè)轉(zhuǎn)換器是SQLExceptionSubclassTranslator public SQLErrorCodeSQLExceptionTranslator() { this.setFallbackTranslator(new SQLExceptionSubclassTranslator()); } //前面生成轉(zhuǎn)換器的時(shí)候,exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); //使用的是本構(gòu)造方法,傳入了DataSource,其中有數(shù)據(jù)庫廠商信息,本文中是MYSQL public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) { this(); this.setDataSource(dataSource); } //從錯(cuò)誤碼工廠SQLErrorCodesFactory里,獲取和數(shù)據(jù)源對(duì)應(yīng)的廠商的所有錯(cuò)誤碼 public void setDataSource(DataSource dataSource) { this.sqlErrorCodes = SingletonSupplier.of(() -> { return SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource); }); this.sqlErrorCodes.get(); } }
錯(cuò)誤碼工廠SQLErrorCodesFactory的resolveErrorCodes方法:
//既然是工廠,里面肯定有各種數(shù)據(jù)庫的錯(cuò)誤碼,本文中使用的是MYSQL,我們看一下實(shí)現(xiàn)邏輯 @Nullable public SQLErrorCodes resolveErrorCodes(DataSource dataSource) { Assert.notNull(dataSource, "DataSource must not be null"); if (logger.isDebugEnabled()) { logger.debug("Looking up default SQLErrorCodes for DataSource [" + this.identify(dataSource) + "]"); } //從緩存中拿MYSQL對(duì)應(yīng)的SQLErrorCodes SQLErrorCodes sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource); if (sec == null) { synchronized(this.dataSourceCache) { sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource); if (sec == null) { try { String name = (String)JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductName); if (StringUtils.hasLength(name)) { SQLErrorCodes var10000 = this.registerDatabase(dataSource, name); return var10000; } } catch (MetaDataAccessException var6) { logger.warn("Error while extracting database name", var6); } return null; } } } if (logger.isDebugEnabled()) { logger.debug("SQLErrorCodes found in cache for DataSource [" + this.identify(dataSource) + "]"); } return sec; }
緩存dataSourceCache如何生成的?
public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) { //根據(jù)數(shù)據(jù)庫類型名稱(這里是MySQL),獲取錯(cuò)誤碼列表 SQLErrorCodes sec = this.getErrorCodes(databaseName); if (logger.isDebugEnabled()) { logger.debug("Caching SQL error codes for DataSource [" + this.identify(dataSource) + "]: database product name is '" + databaseName + "'"); } this.dataSourceCache.put(dataSource, sec); return sec; } public SQLErrorCodes getErrorCodes(String databaseName) { Assert.notNull(databaseName, "Database product name must not be null"); //從errorCodesMap根據(jù)key=MYSQL獲取SQLErrorCodes SQLErrorCodes sec = (SQLErrorCodes)this.errorCodesMap.get(databaseName); if (sec == null) { Iterator var3 = this.errorCodesMap.values().iterator(); while(var3.hasNext()) { SQLErrorCodes candidate = (SQLErrorCodes)var3.next(); if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) { sec = candidate; break; } } } if (sec != null) { this.checkCustomTranslatorRegistry(databaseName, sec); if (logger.isDebugEnabled()) { logger.debug("SQL error codes for '" + databaseName + "' found"); } return sec; } else { if (logger.isDebugEnabled()) { logger.debug("SQL error codes for '" + databaseName + "' not found"); } return new SQLErrorCodes(); } } //SQLErrorCodesFactory構(gòu)造方法中,生成的errorCodesMap,map的內(nèi)容來自org/springframework/jdbc/support/sql-error-codes.xml文件 protected SQLErrorCodesFactory() { Map errorCodes; try { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); lbf.setBeanClassLoader(this.getClass().getClassLoader()); XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf); Resource resource = this.loadResource("org/springframework/jdbc/support/sql-error-codes.xml"); if (resource != null && resource.exists()) { bdr.loadBeanDefinitions(resource); } else { logger.info("Default sql-error-codes.xml not found (should be included in spring-jdbc jar)"); } resource = this.loadResource("sql-error-codes.xml"); if (resource != null && resource.exists()) { bdr.loadBeanDefinitions(resource); logger.debug("Found custom sql-error-codes.xml file at the root of the classpath"); } errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false); if (logger.isTraceEnabled()) { logger.trace("SQLErrorCodes loaded: " + errorCodes.keySet()); } } catch (BeansException var5) { logger.warn("Error loading SQL error codes from config file", var5); errorCodes = Collections.emptyMap(); } this.errorCodesMap = errorCodes; }
sql-error-codes.xml文件中配置了各個(gè)數(shù)據(jù)庫的主要的錯(cuò)誤碼
這里列舉了MYSQL部分,當(dāng)然還有其他部分,我們可以看到唯一性約束錯(cuò)誤碼是1062,就可以翻譯成DuplicateKeyException異常了
<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>
你已經(jīng)看到,比如上面的錯(cuò)誤碼值列舉了一部分,如果出現(xiàn)了一個(gè)不在其中的錯(cuò)誤碼肯定是匹配不到,Spring當(dāng)然能想到這種情況了
/** *@公-眾-號(hào):程序員阿牛 *在AbstractFallbackSQLExceptionTranslator中,看到如果查找失敗會(huì)獲取下一個(gè)后續(xù)轉(zhuǎn)換器 */ @Nullable public DataAccessException translate(String task, @Nullable String sql, SQLException ex) { Assert.notNull(ex, "Cannot translate a null SQLException"); DataAccessException dae = this.doTranslate(task, sql, ex); if (dae != null) { return dae; } else { SQLExceptionTranslator fallback = this.getFallbackTranslator(); return fallback != null ? fallback.translate(task, sql, ex) : null; } }
SQLErrorCodeSQLExceptionTranslator的后置轉(zhuǎn)換器是什么?
//構(gòu)造方法中已經(jīng)指定,SQLExceptionSubclassTranslator public SQLErrorCodeSQLExceptionTranslator() { this.setFallbackTranslator(new SQLExceptionSubclassTranslator()); }
SQLExceptionSubclassTranslator的轉(zhuǎn)換方法邏輯如下:
/** *@公-眾-號(hào):程序員阿牛 *可以看出實(shí)際按照子類類型來判斷,返回相應(yīng)的錯(cuò)誤類,如果匹配不到,則找到下一個(gè)處理器,這里的處理其我們可以根據(jù)構(gòu)造方法青松找到*SQLStateSQLExceptionTranslator */ @Nullable protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { if (ex instanceof SQLTransientException) { if (ex instanceof SQLTransientConnectionException) { return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex); } if (ex instanceof SQLTransactionRollbackException) { return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex); } if (ex instanceof SQLTimeoutException) { return new QueryTimeoutException(this.buildMessage(task, sql, ex), ex); } } else if (ex instanceof SQLNonTransientException) { if (ex instanceof SQLNonTransientConnectionException) { return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex); } if (ex instanceof SQLDataException) { return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex); } if (ex instanceof SQLIntegrityConstraintViolationException) { return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex); } if (ex instanceof SQLInvalidAuthorizationSpecException) { return new PermissionDeniedDataAccessException(this.buildMessage(task, sql, ex), ex); } if (ex instanceof SQLSyntaxErrorException) { return new BadSqlGrammarException(task, sql != null ? sql : "", ex); } if (ex instanceof SQLFeatureNotSupportedException) { return new InvalidDataAccessApiUsageException(this.buildMessage(task, sql, ex), ex); } } else if (ex instanceof SQLRecoverableException) { return new RecoverableDataAccessException(this.buildMessage(task, sql, ex), ex); } return null; }
SQLStateSQLExceptionTranslator的轉(zhuǎn)換方法:
/** *@公-眾-號(hào):程序員阿牛 *可以看出根據(jù)SQLState的前兩位來判斷異常,根據(jù)匹配結(jié)果返回相應(yīng)的異常信息 */ @Nullable protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { String sqlState = this.getSqlState(ex); if (sqlState != null && sqlState.length() >= 2) { String classCode = sqlState.substring(0, 2); if (this.logger.isDebugEnabled()) { this.logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'"); } if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) { return new BadSqlGrammarException(task, sql != null ? sql : "", ex); } if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) { return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex); } if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) { return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex); } if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) { return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex); } if (CONCURRENCY_FAILURE_CODES.contains(classCode)) { return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex); } } return ex.getClass().getName().contains("Timeout") ? new QueryTimeoutException(this.buildMessage(task, sql, ex), ex) : null; }
為什么SQLState可以得出錯(cuò)誤類型?
因?yàn)閿?shù)據(jù)庫是根據(jù) X/Open 和 SQL Access Group SQL CAE 規(guī)范 (1992) 所進(jìn)行的定義,SQLERROR 返回 SQLSTATE 值。SQLSTATE 值是包含五個(gè)字符的字符串 。五個(gè)字符包含數(shù)值或者大寫字母, 代表各種錯(cuò)誤或者警告條件的代碼。SQLSTATE 有個(gè)層次化的模式:頭兩個(gè)字符標(biāo)識(shí)條件的通常表示錯(cuò)誤條件的類別, 后三個(gè)字符表示在該通用類中的子類。成功的狀態(tài)是由 00000 標(biāo)識(shí)的。SQLSTATE 代碼在大多數(shù)地方都是定義在 SQL 標(biāo)準(zhǔn)里
處理流程圖
用到了哪些設(shè)計(jì)模式?
組合模式
通過上圖大家有沒有發(fā)現(xiàn)三個(gè)實(shí)現(xiàn)類之間的關(guān)系—組合關(guān)系,組合關(guān)系在父類AbstractFallbackSQLExceptionTranslator中變成了遞歸調(diào)用,這里充滿了智慧(Composite設(shè)計(jì)模式)。
單例模式
在SQLErrorCodesFactory(單例模式)
策略模式
根據(jù)數(shù)據(jù)庫的不同,獲取不同的errorcodes集合
總結(jié):
在學(xué)習(xí)的過程中,我們不但要關(guān)注其實(shí)現(xiàn)的方式,還要關(guān)注我們能從里面學(xué)到什么?比如說從這個(gè)異常抽象中,能學(xué)到幾種設(shè)計(jì)模式,以及使用的場(chǎng)景,這些都是可以運(yùn)用到以后的工作中。
到此這篇關(guān)于詳解Spring不同數(shù)據(jù)庫異常如何抽象的文章就介紹到這了,更多相關(guān)Spring數(shù)據(jù)庫異常抽象內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解SpringBoot如何實(shí)現(xiàn)多環(huán)境配置
在實(shí)際的軟件開發(fā)過程中,一個(gè)應(yīng)用程序通常會(huì)有多個(gè)環(huán)境,pring?Boot?提供了一個(gè)非常靈活和強(qiáng)大的方式來管理這些環(huán)境配置,下面就跟隨小編一起學(xué)習(xí)一下吧2023-07-07Java使用ant.jar執(zhí)行SQL腳本文件的示例代碼
這篇文章主要介紹了Java使用ant.jar執(zhí)行SQL腳本文件,文中通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-02-02Java高并發(fā)之CyclicBarrier的用法詳解
CyclicBarrier 是 Java 中的一種同步工具,它可以讓多個(gè)線程在一個(gè)屏障點(diǎn)處等待,直到所有線程都到達(dá)該點(diǎn)后,才能繼續(xù)執(zhí)行。本文就來和大家聊聊它的用法,需要的可以參考一下2023-03-03SSM項(xiàng)目中使用攔截器和過濾器的實(shí)現(xiàn)示例
這篇文章主要介紹了SSM項(xiàng)目中使用攔截器和過濾器的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04IDEA如何修改項(xiàng)目名稱出現(xiàn)中括號(hào)
當(dāng)項(xiàng)目文件夾名稱與model名稱不一致時(shí),IDEA會(huì)在項(xiàng)目名旁顯示中括號(hào)以區(qū)分,修改項(xiàng)目名稱時(shí)出現(xiàn)中括號(hào)問題,通常是因?yàn)镮DE中model名與文件夾名不同步,解決方法是統(tǒng)一model名稱和文件夾名稱,可通過重構(gòu)功能或項(xiàng)目結(jié)構(gòu)設(shè)置完成,重命名操作應(yīng)謹(jǐn)慎,以避免路徑等引用錯(cuò)誤2024-10-10Java實(shí)現(xiàn)經(jīng)典拳皇誤闖冒險(xiǎn)島游戲的示例代碼
《拳皇誤闖冒險(xiǎn)島》是拳皇和冒險(xiǎn)島素材的基于JavaSwing的動(dòng)作類游戲。本文將用Java語言實(shí)現(xiàn)這一游戲,需要的小伙伴可以參考一下2022-02-02SpringCloud Zuul在何種情況下使用Hystrix及問題小結(jié)
這篇文章主要介紹了SpringCloud Zuul在何種情況下使用Hystrix 及問題小結(jié),感興趣的朋友跟隨小編一起看看吧2018-11-11