欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring異常處理?bug的問題記錄(同一份代碼,結(jié)果卻不一樣)

 更新時(shí)間:2025年05月26日 08:58:47   作者:程序員博博  
這篇文章主要介紹了Spring異常處理?bug的問題記錄(同一份代碼,結(jié)果卻不一樣)的相關(guān)資料,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧

1. 背景

在上周遇到一個(gè)spring bug的問題,將其記錄一下。簡(jiǎn)化的代碼如下:

public void insert() {
    try {
        Person person = new Person();
        person.setId(3581L);// 這個(gè)是主鍵,擁有唯一索引**
        personDao.insert(person);
    } catch (DuplicateKeyException e) {
        log.error("DuplicateKeyException e = {}", e.getMessage(), e);
        // DuplicateKeyException 其他邏輯處理
    } catch (DataIntegrityViolationException e) {
        log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
        // DataIntegrityViolationException 其他邏輯處理
    } catch (Exception e) {
        log.error("Exception e = {}", e.getMessage(), e);
    }
}

然而同一份代碼,部署在不同機(jī)器(數(shù)據(jù)庫(kù)只有一個(gè), 不存在分庫(kù)分表情況),遇到的情況不一樣。

A機(jī)器:如果主鍵沖突,則拋出DuplicateKeyException異常,進(jìn)入第7行的邏輯

B機(jī)器:如果主鍵沖突,則拋出DataIntegrityViolationException異常,進(jìn)入第11行的邏輯

甚至我將B機(jī)器重啟,如果主鍵沖突,則拋出DuplicateKeyException異常,進(jìn)入第7行的邏輯

非常的奇怪,我們一一細(xì)說

2. 數(shù)據(jù)庫(kù)異常分析

2.1 spring對(duì)java標(biāo)準(zhǔn)異常的包裝

異常類型/屬性所屬框架或技術(shù)棧觸發(fā)場(chǎng)景
SQLIntegrityConstraintViolationException屬于 JDBC 標(biāo)準(zhǔn)異常體系,是 java.sql.SQLException 的子類。當(dāng)數(shù)據(jù)庫(kù)操作違反了完整性約束(如主鍵沖突、外鍵約束、唯一性約束等)時(shí),JDBC 驅(qū)動(dòng)會(huì)拋出此異常。
DuplicateKeyException是 Spring 框架中定義的異常,屬于 Spring Data 或 Spring JDBC 的封裝異常。通常在插入或更新數(shù)據(jù)時(shí),違反了數(shù)據(jù)庫(kù)表的主鍵或唯一索引約束(即嘗試插入重復(fù)的主鍵或唯一鍵值)。
DataIntegrityViolationException是 Spring 框架中的異常,屬于 Spring 數(shù)據(jù)訪問層的通用異常體系是一個(gè)更通用的異常,表示任何違反數(shù)據(jù)完整性的操作,包括但不限于主鍵沖突、外鍵約束、非空約束等

從表格中我們可以明顯看出,SQLIntegrityConstraintViolationException是屬于Java體系的標(biāo)準(zhǔn)異常,當(dāng)主鍵沖突,外鍵約束,非空等情況正常都會(huì)拋出這個(gè)異常

然后spring框架對(duì)這個(gè)異常進(jìn)行了一個(gè)封裝,比如違反唯一索引會(huì)拋出DuplicateKeyException異常,其他的情況會(huì)拋出DataIntegrityViolationException異常。

2.2 spring代碼包裝

在spring中會(huì)有一個(gè)SQLErrorCodesFactory類,會(huì)加載下面路徑下的資源。也就是說,每個(gè)數(shù)據(jù)庫(kù)廠商對(duì)于不同異常返回的錯(cuò)誤碼不同,spring進(jìn)行了一個(gè)包裝

public static final String SQL_ERROR_CODE_DEFAULT_PATH 
    =  "org/springframework/jdbc/support/sql-error-codes.xml";

2.3 問題產(chǎn)生的原因

在spring異常處理中,有一個(gè)非常核心的類 SQLErrorCodeSQLExceptionTranslator,但遇到主鍵沖突,非空約束等異常的時(shí)候,spring會(huì)使用這個(gè)類進(jìn)行轉(zhuǎn)化。

if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
    logTranslation(task, sql, sqlEx, false);
    return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
    logTranslation(task, sql, sqlEx, false);
    return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx);
}
else if (Arrays.binarySearch( this .sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
    logTranslation(task, sql, sqlEx, false);
    return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
    logTranslation(task, sql, sqlEx, false);
    return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
}
else if // xxx 省略

我們可以從上面代碼中可以看到,他其中是從sqlErrorCodes中,進(jìn)行二分查找,是否存在相應(yīng)的code碼,然后返回給上游不同的錯(cuò)誤,那么sqlErrorCodes是從哪里獲取的呢。

try {
    String name = JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName");
    if (StringUtils.hasLength(name)) {
       return registerDatabase(dataSource, name);
    }
}
catch (MetaDataAccessException ex) {
    logger.warn("Error while extracting database name - falling back to empty error codes", ex);
}
// Fallback is to return an empty SQLErrorCodes instance.
return new SQLErrorCodes();

從上面代碼我們可以看出,會(huì)通過JdbcUtils.extractDatabaseMetaData方法來獲取sqlErrorCodes,是哪個(gè)廠商,并且獲取到Connection進(jìn)行連接,然后返回相應(yīng)的sqlErrorCodes碼

但是在第7行,如果此時(shí)Connection數(shù)據(jù)庫(kù)鏈接有異常,則會(huì)報(bào)錯(cuò),然后返回11行一個(gè)空的sqlErrorCodes,那么問題就出在這里了?。?!

也就是說,如果在第一次獲取sqlErrorCodes,如果出了問題,那么這個(gè)字段就會(huì)為空,上面代碼的轉(zhuǎn)化異常邏輯就會(huì)判斷錯(cuò)誤。就會(huì)走到else兜底退避的策略。

具體退避的策略在SQLExceptionSubclassTranslator類中,所以當(dāng)走到了退避策略,所有SQLIntegrityConstraintViolationException異常都會(huì)返回DataIntegrityViolationException異常

if (ex instanceof SQLNonTransientConnectionException) {
    return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLDataException) {
    return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if (ex instanceof SQLIntegrityConstraintViolationException) {
    return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
}
else if // 省略

3. 問題復(fù)現(xiàn)

3.1 錯(cuò)誤復(fù)現(xiàn)

我們從2.3分析中,可以清楚的知道,根因是SQLErrorCodeSQLExceptionTranslator類中sqlErrorCodes字段為空導(dǎo)致主鍵沖突退避返回了DataIntegrityViolationException異常。

那么我們就可以模擬鏈接異常,比如連接被關(guān)閉了,導(dǎo)致首次初始化的時(shí)候?qū)е聅qlErrorCodes失敗,代碼如下 (注意這塊代碼必須在項(xiàng)目啟動(dòng) 首先第一次執(zhí)行)

@Transactional
public void testConnect() {
    try {
        Connection connection = DataSourceUtils.getConnection(dataSource);
        connection.close(); // 強(qiáng)制關(guān)閉連接,破壞事務(wù)一致性
        personDao.selectById(1L);
    } catch (DuplicateKeyException e) {
        log.error("DuplicateKeyException e = {}", e.getMessage(), e);
    } catch (DataIntegrityViolationException e) {
        log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
    } catch (Exception e) {
        log.error("Exception e = {}", e.getMessage(), e);
    }
}

在上面代碼中,我們獲取了鏈接,并且強(qiáng)制關(guān)閉了,那么就會(huì)導(dǎo)致初始化的時(shí)候走2.3那塊代碼就會(huì)報(bào)錯(cuò),此時(shí)sqlErrorCodes就會(huì)為空。

如果后面sql遇到了唯一索引,返回如下:

3.2 正確復(fù)現(xiàn)

將上面代碼connection.close()去掉,那么第一次緩存就正常了。再次執(zhí)行,如果遇到了唯一索引,返回如下:

4. 解決辦法

在github上面已經(jīng)有人提出此問題,并且標(biāo)記為了bug,鏈接如下:https://github.com/spring-projects/spring-framework/issues/25681

并且修復(fù)pull request如下 (此代碼已合并到v5.2.9.RELEASE分支)

https://github.com/spring-projects/spring-framework/commit/670b9fd60b3b5ada69b060424d697270eeee01c2#diff-e2f38c7b7d44c3679cd585e5c81e76b3ca32313bf870caa6435cd36bfe4d9600

4.1 辦法1

升級(jí)spring版本到5.2.9.release+,可以徹底解決此問題

4.2 辦法2

第一步在項(xiàng)目啟動(dòng)的時(shí)候,獲取SQLErrorCodes,如果為空,則打印error日志并且告警。讓開發(fā)同學(xué)知道有這么一個(gè)問題 (可重啟也可不重啟)

public class DatabaseMetadataPreloader  {
    @PostConstruct
    public void init() {
       try {
          SQLErrorCodes errorCodes = errorCodesFactory.getErrorCodes(dataSource);
          log.info("Database metadata preloaded successfully errorCodes = {}", GsonUtils.toJson(errorCodes));
          String[] duplicateKeyCodes = errorCodes.getDuplicateKeyCodes();
          if (ArrayUtils.isEmpty(duplicateKeyCodes)) {
             log.error("No duplicate key codes found in database metadata 請(qǐng)重啟服務(wù)");
          }
       } catch (Exception e) {
          log.error("Failed to preload database metadata", e);
       }
    }
}

第二步重新查詢一遍數(shù)據(jù)庫(kù)

如果有數(shù)據(jù)則表明是索引沖突,如果沒有數(shù)據(jù),則可能是其他異常引起的,走原有的老邏輯

catch (DuplicateKeyException e) {
    log.error("DuplicateKeyException e = {}", e.getMessage(), e);
}
catch (DataIntegrityViolationException e) {
    log.error("DataIntegrityViolationException e = {}", e.getMessage(), e);
    // 重新查一遍數(shù)據(jù)庫(kù),如果有數(shù)據(jù),說明是唯一索引沖突
    Person p = select(xxxx)
    if (p != null) {
        // 唯一索引沖突
    } else {
        // 其他異常引起的
    }
}

相關(guān)文章

  • Python import與from import使用及區(qū)別介紹

    Python import與from import使用及區(qū)別介紹

    Python程序可以調(diào)用一組基本的函數(shù)(即內(nèi)建函數(shù)),比如print()、input()和len()等函數(shù)。接下來通過本文給大家介紹Python import與from import使用及區(qū)別介紹,感興趣的朋友一起看看吧
    2018-09-09
  • python跳出雙層for循環(huán)的解決方法

    python跳出雙層for循環(huán)的解決方法

    今天小編就為大家分享一篇python跳出雙層for循環(huán)的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2019-06-06
  • Python使用PyCrypto實(shí)現(xiàn)AES加密功能示例

    Python使用PyCrypto實(shí)現(xiàn)AES加密功能示例

    這篇文章主要介紹了Python使用PyCrypto實(shí)現(xiàn)AES加密功能,結(jié)合具體實(shí)例形式分析了PyCrypto實(shí)現(xiàn)AES加密的操作步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2017-05-05
  • 如何利用Python監(jiān)控別人的網(wǎng)站

    如何利用Python監(jiān)控別人的網(wǎng)站

    這篇文章主要為大家詳細(xì)介紹了如何利用Python實(shí)現(xiàn)監(jiān)控別人的網(wǎng)站,這樣還可以詳細(xì)了解你的競(jìng)爭(zhēng)對(duì)手網(wǎng)站,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-05-05
  • Django中的Signal代碼詳解

    Django中的Signal代碼詳解

    這篇文章主要介紹了Django中的Signal代碼詳解,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2018-02-02
  • django使用LDAP驗(yàn)證的方法示例

    django使用LDAP驗(yàn)證的方法示例

    這篇文章主要介紹了django使用LDAP驗(yàn)證的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-12-12
  • Django-xadmin+rule對(duì)象級(jí)權(quán)限的實(shí)現(xiàn)方式

    Django-xadmin+rule對(duì)象級(jí)權(quán)限的實(shí)現(xiàn)方式

    今天小編就為大家分享一篇Django-xadmin+rule對(duì)象級(jí)權(quán)限的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-03-03
  • Python中動(dòng)態(tài)獲取對(duì)象的屬性和方法的教程

    Python中動(dòng)態(tài)獲取對(duì)象的屬性和方法的教程

    本文主要介紹了如何在Python中動(dòng)態(tài)獲取對(duì)象的屬性和方法,并運(yùn)行使用它們,需要的朋友可以參考一下
    2015-04-04
  • python主動(dòng)拋出異常raise的方法實(shí)現(xiàn)

    python主動(dòng)拋出異常raise的方法實(shí)現(xiàn)

    本文主要介紹了python主動(dòng)拋出異常raise的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-12-12
  • Python文本預(yù)處理學(xué)習(xí)指南

    Python文本預(yù)處理學(xué)習(xí)指南

    文本預(yù)處理是指在進(jìn)行自然語(yǔ)言處理(NLP)任務(wù)之前,對(duì)原始文本數(shù)據(jù)進(jìn)行清洗、轉(zhuǎn)換和標(biāo)準(zhǔn)化的過程,本文主要為大家介紹了文本預(yù)處理的使用,需要的可以參考下
    2023-07-07

最新評(píng)論