您好,登錄后才能下訂單哦!
本篇內容介紹了“Spring轉換SQLException并埋入擴展點的工作過程”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
我不知道這樣理解是否正確:
SQLState 的來源是對 SQL 標準的履約。盡管 SQL 數據庫的廠商有很多,只要它們都尊重 SQL 標準和 JDBC 的規范,那么不同廠商在同樣的錯誤上必然返回同樣的 SQLState。
vendorCode 很魔幻,它對應的 getter 方法名為 getErrorCode。但字段名里的 vendor 和它的 getter 方法上的注解已經將出賣了它的意義。vendorCode 或者說 errorCode 并不是 SQL 標準的內容,不同數據庫廠商可能對同樣的錯誤返回不同的 errorCode。
我本來猜測默認的轉換器 org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
在 SpringBoot 環境下,在引入 spring-boot-starter-jdbc
依賴后會在某個 *AutoConfiguration 中被一個帶有 @Bean
的方法注冊,這個方法同時可能還將也注冊為 Bean 的 SQLErrorCodes
通過 setter 注入。
然而后者,即默認的 SQLErrorCodes
的注冊為 Bean 的地方我找到了, org\springframework\jdbc\support\sql-error-codes.xml
,在這個里面注冊并注入了一系列屬性的。然而我對 SQLErrorCodeSQLExceptionTranslator
和它的父類借助IDEA的 Alt + F7
檢索被使用的地方并沒發現上一段說的注冊它為 Bean 的方法。如果有老哥知道它在哪兒成為 Bean 加入上下文環境的告訴我一聲。我先把假設當事實用著了。
請看正文的第一段。
org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
,這是 Spring 默認提供的轉換各種 SQLException 異常為Spring 內置的統一的異常類型的轉換器。本文主要通過它,準確地說是它的 protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex)
方法來看一看它整個的處理思路以及其中給用戶流出的擴展點。
SQLException sqlEx = ex; if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) { SQLException nestedSqlEx = sqlEx.getNextException(); if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) { sqlEx = nestedSqlEx; } }
上面是 doTranslate 方法入門第一段,它做的事情很簡單,起手先判斷這個異常是不是 BatchUpdateException
類型的從名字上猜,這個異常只會在批處理時拋出。而之后的sqlEx.getNextException()
,我們追進源碼去看,有這些重要相關代碼:
/** * Retrieves the exception chained to this * <code>SQLException</code> object by setNextException(SQLException ex). * * @return the next <code>SQLException</code> object in the chain; * <code>null</code> if there are none * @see #setNextException */ public SQLException getNextException() { return (next); } /** * Adds an <code>SQLException</code> object to the end of the chain. * * @param ex the new exception that will be added to the end of * the <code>SQLException</code> chain * @see #getNextException */ public void setNextException(SQLException ex) { SQLException current = this; for(;;) { SQLException next=current.next; if (next != null) { current = next; continue; } if (nextUpdater.compareAndSet(current,null,ex)) { return; } current=current.next; } }
一閱讀 setNextException
就知道,玩鏈表的老手了,next 一定永遠指向鏈表最末元素。
那在看回轉換器的代碼,那么它做的事情就是:取出異常鏈的最末尾異常,然后判斷 nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null
,如果通過,則 sqlEx
就置為這個最末尾異常。而閱讀之后的代碼后能知道,sqlEx
就是會被 Spring 轉換處理的目標。
也就是說只要末尾的SQLException是個正常的異常,Spring 就只關心末尾的異常了。這是為什么?
另外,為什么 SQLException 要搞一個異常鏈呢?頂層 Exception 不是已經設置了 cause 這樣一個機制來實現異常套娃嗎?
// First, try custom translation from overridden method. DataAccessException dae = customTranslate(task, sql, sqlEx); if (dae != null) { return dae; }
customTranslate
這個方法光聽名字就很有擴展點的感覺,跳轉過去看下它的代碼:
@Nullable protected DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlEx) { return null; }
第一個埋入的擴展點出現了。這個方法的存在,使得我們可以通過繼承 SQLErrorCodeSQLExceptionTranslator
類進行擴展,裝入自己的私活。我猜測注冊 SQLErrorCodeSQLExceptionTranslator
到環境中的那個 Bean 方法應該是有 @ConditionalOnMissingBean
在的,我們手動將自己實現的繼承 SQLErrorCodeSQLExceptionTranslator
的類注冊為 Bean 后它自己就不會再注冊了,從而實現偷換轉換器夾帶私活。
// Next, try the custom SQLException translator, if available. SQLErrorCodes sqlErrorCodes = getSqlErrorCodes(); if (sqlErrorCodes != null) { SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator(); if (customTranslator != null) { DataAccessException customDex = customTranslator.translate(task, sql, sqlEx); if (customDex != null) { return customDex; } } }
SQLErrorCodes
就是之前在 org\springframework\jdbc\support\sql-error-codes.xml
注冊的 Bean 的類。而在那個 xml 文件的開頭,人家明確說了:
- Default SQL error codes for well-known databases. - Can be overridden by definitions in a "sql-error-codes.xml" file - in the root of the class path.
如果你在自己的項目的 class path 下寫一個 sql-error-codes.xml
,那么 Spring 默認提供的就會被覆蓋。
注意 sqlErrorCodes.getCustomSqlExceptionTranslator()
。這一步從一個 Bean 中取出它的一個成員,而這個成員是完全有 getter 顯然也有 setter,也就意味著它可以在 SQLErrorCodes
注冊為 Bean 的同時的一個成員 Bean 注入。
結合 sql-error-codes.xml
頭部的注釋,于是有:用戶先寫一個夾帶自己私活的 SQLExceptionTranslator
接口的實現類。然后自己在項目 class path 下新建一個 sql-error-codes.xml
,復制 Spring 已經提供的內容。再然后,在 xml 里將自己的私活 SQLExceptionTranslator
實現類注冊為 Bean,并在復制過來的注冊 SQLErrorCodes
為 Bean 的內容里添加一條 customSqlExceptionTranslator
的成員的注入,用的當然是自己的那個私活 Bean。這樣就完成了擴展。
其實這里還有第三個擴展點。注意第一行的 getSqlErrorCodes();
,有這個 getter 也有 setter 還有 sqlErrorCodes
的成員,這里 SQLErrorCodes
也是后注入給 ``SQLErrorCodeSQLExceptionTranslator的組件。那我們也可以自定義一個
SQLErrorCodes的實現類然后注冊為 Bean,取代默認的。那
SQLErrorCodes` 都徹底換了個,夾帶私活肯定沒問題了。
第四段也是最后一段,這段比較長,段內部還需分片看。
// Check SQLErrorCodes with corresponding error code, if available. if (sqlErrorCodes != null) { String errorCode; if (sqlErrorCodes.isUseSqlStateForTranslation()) { errorCode = sqlEx.getSQLState(); } else { // Try to find SQLException with actual error code, looping through the causes. // E.g. applicable to java.sql.DataTruncation as of JDK 1.6. SQLException current = sqlEx; while (current.getErrorCode() == 0 && current.getCause() instanceof SQLException) { current = (SQLException) current.getCause(); } errorCode = Integer.toString(current.getErrorCode()); }
這一段是做了一些準備工作。
/** * Set this property to true for databases that do not provide an error code * but that do provide SQL State (this includes PostgreSQL). */ public void setUseSqlStateForTranslation(boolean useStateCodeForTranslation) { this.useSqlStateForTranslation = useStateCodeForTranslation; } public boolean isUseSqlStateForTranslation() { return this.useSqlStateForTranslation; }
從 setUseSqlStateForTranslation
方法的注釋我們可以推測,isUseSqlStateForTranslation()
返回 true
時,數據庫廠商是那種 JDBC 執行出錯不返回 errorCode 只返回 SQLState 的,兩者都返回的這里應該返回 false
(兩者都不返回的那不是正常的 JDBC 實現)。在此基礎上繼續回去理解代碼,那這里就是獲取待轉化的異常中最有價值的可以表明自己錯誤的異常的對應的錯誤碼置給 errorCode
。而后續操作都基于這個 errorCode
。
if (errorCode != null) { // Look for defined custom translations first. CustomSQLErrorCodesTranslation[] customTranslations = sqlErrorCodes.getCustomTranslations(); if (customTranslations != null) { for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) { if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 && customTranslation.getExceptionClass() != null) { DataAccessException customException = createCustomException( task, sql, sqlEx, customTranslation.getExceptionClass()); if (customException != null) { logTranslation(task, sql, sqlEx, true); return customException; } } } }
這個第四個擴展點,就是可以注入到 SQLErrorCodes
中的 CustomSQLErrorCodesTranslation[]
,細看 CustomSQLErrorCodesTranslation
這個類:
/** * JavaBean for holding custom JDBC error codes translation for a particular * database. The "exceptionClass" property defines which exception will be * thrown for the list of error codes specified in the errorCodes property. * * @author Thomas Risberg * @since 1.1 * @see SQLErrorCodeSQLExceptionTranslator */ public class CustomSQLErrorCodesTranslation { private String[] errorCodes = new String[0]; @Nullable private Class<?> exceptionClass;
我沒復制粘貼完,因為這些已經夠了,看注釋:
The "exceptionClass" property defines which exception will be thrown for the list of error codes specified in the errorCodes property.
回到第四段對 CustomSQLErrorCodesTranslation[]
的使用:
for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) { if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 && customTranslation.getExceptionClass() != null)
對 CustomSQLErrorCodesTranslation[]
中的每一個 customTranslations
,匹配當前的 errorCode
是不是在它的處理范圍內(即它的 private String[] errorCodes
這個數組類型的成員中有和 errorCode
相同的值),如果有,且它 private Class<?> exceptionClass
成員不為 null
,就說明該將當前異常轉化為 exceptionClass
類的異常。
話說我一開始寫這篇文章的動機,就是極客時間-玩轉Spring全家桶-了解Spring的JDBC抽象異常里講,而我想具體搞明白。而課程視頻就是在自定義的``sql-error-codes.xml中注入自定義的
CustomSQLErrorCodesTranslation[]`來完成擴展的。
// Next, look for grouped error codes. if (Arrays.binarySearch(sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) { logTranslation(task, sql, sqlEx, false); return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx); } else if (Arrays.binarySearch(sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) { logTranslation(task, sql, sqlEx, false); return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx); } //后面還有大同小異邏輯一致的幾個就不復制粘貼了
這一段的大體邏輯和上一節相同,已經到了最后,沒有了擴展空間。
getBadSqlGrammarCodes()
,getInvalidResultSetAccessCodes()
雖說也能通過自定義的 sql-error-codes.xml
去改,但沒必要了,因為它們返回的異常的類型都是寫死的,這塊地方其實是 Spring 給經典的 SQL 錯誤的自留地,我們就不要動了。寫自己的 sql-error-codes.xml
時復制粘貼下這塊東西就好,如下是 Spring 原生的 sql-error-codes.xml
中對應 MySQL 數據庫的內容:
<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>
后面還有一些代碼就是 Spring 處理不過來的 SQLException 做一些日志記錄,不值得多說了。
第一個擴展點是 protected
的方法,用戶可以通過繼承后在此方法中添加自己的實現的實現后將自己的實現類注冊為 Bean,再結合 Spring 為默認的實現注冊 Bean 時的 @ConditionalOnMissingBean
限制,達成了擴展點。
隨后的幾個擴展點的本質都是為默認實現的 Bean 中留下可注入的成員,用戶通過實現特定接口并將其注冊為 Bean,結合成員注入將帶著自己實現邏輯的 Bean 注入后將自己的私活帶入。
“Spring轉換SQLException并埋入擴展點的工作過程”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。