当前位置: 代码网 > it编程>编程语言>Java > Spring异常处理 bug的问题记录(同一份代码,结果却不一样)

Spring异常处理 bug的问题记录(同一份代码,结果却不一样)

2025年05月26日 Java 我要评论
1. 背景在上周遇到一个spring bug的问题,将其记录一下。简化的代码如下:public void insert() { try { person person = new

1. 背景

在上周遇到一个spring bug的问题,将其记录一下。简化的代码如下:

public void insert() {
    try {
        person person = new person();
        person.setid(3581l);// 这个是主键,拥有唯一索引**
        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);
    }
}

然而同一份代码,部署在不同机器(数据库只有一个, 不存在分库分表情况),遇到的情况不一样。

a机器:如果主键冲突,则抛出duplicatekeyexception异常,进入第7行的逻辑

b机器:如果主键冲突,则抛出dataintegrityviolationexception异常,进入第11行的逻辑

甚至我将b机器重启,如果主键冲突,则抛出duplicatekeyexception异常,进入第7行的逻辑

非常的奇怪,我们一一细说

2. 数据库异常分析

2.1 spring对java标准异常的包装

异常类型/属性所属框架或技术栈触发场景
sqlintegrityconstraintviolationexception属于 jdbc 标准异常体系,是 java.sql.sqlexception 的子类。当数据库操作违反了完整性约束(如主键冲突、外键约束、唯一性约束等)时,jdbc 驱动会抛出此异常。
duplicatekeyexception是 spring 框架中定义的异常,属于 spring data 或 spring jdbc 的封装异常。通常在插入或更新数据时,违反了数据库表的主键或唯一索引约束(即尝试插入重复的主键或唯一键值)。
dataintegrityviolationexception是 spring 框架中的异常,属于 spring 数据访问层的通用异常体系是一个更通用的异常,表示任何违反数据完整性的操作,包括但不限于主键冲突、外键约束、非空约束等

从表格中我们可以明显看出,sqlintegrityconstraintviolationexception是属于java体系的标准异常,当主键冲突,外键约束,非空等情况正常都会抛出这个异常

然后spring框架对这个异常进行了一个封装,比如违反唯一索引会抛出duplicatekeyexception异常,其他的情况会抛出dataintegrityviolationexception异常。

2.2 spring代码包装

在spring中会有一个sqlerrorcodesfactory类,会加载下面路径下的资源。也就是说,每个数据库厂商对于不同异常返回的错误码不同,spring进行了一个包装

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

2.3 问题产生的原因

在spring异常处理中,有一个非常核心的类 sqlerrorcodesqlexceptiontranslator,但遇到主键冲突,非空约束等异常的时候,spring会使用这个类进行转化。

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中,进行二分查找,是否存在相应的code码,然后返回给上游不同的错误,那么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();

从上面代码我们可以看出,会通过jdbcutils.extractdatabasemetadata方法来获取sqlerrorcodes,是哪个厂商,并且获取到connection进行连接,然后返回相应的sqlerrorcodes码

但是在第7行,如果此时connection数据库链接有异常,则会报错,然后返回11行一个空的sqlerrorcodes,那么问题就出在这里了!!!

也就是说,如果在第一次获取sqlerrorcodes,如果出了问题,那么这个字段就会为空,上面代码的转化异常逻辑就会判断错误。就会走到else兜底退避的策略。

具体退避的策略在sqlexceptionsubclasstranslator类中,所以当走到了退避策略,所有sqlintegrityconstraintviolationexception异常都会返回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. 问题复现

3.1 错误复现

我们从2.3分析中,可以清楚的知道,根因是sqlerrorcodesqlexceptiontranslator类中sqlerrorcodes字段为空导致主键冲突退避返回了dataintegrityviolationexception异常。

那么我们就可以模拟链接异常,比如连接被关闭了,导致首次初始化的时候导致sqlerrorcodes失败,代码如下 (注意这块代码必须在项目启动 首先第一次执行)

@transactional
public void testconnect() {
    try {
        connection connection = datasourceutils.getconnection(datasource);
        connection.close(); // 强制关闭连接,破坏事务一致性
        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);
    }
}

在上面代码中,我们获取了链接,并且强制关闭了,那么就会导致初始化的时候走2.3那块代码就会报错,此时sqlerrorcodes就会为空。

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

3.2 正确复现

将上面代码connection.close()去掉,那么第一次缓存就正常了。再次执行,如果遇到了唯一索引,返回如下:

4. 解决办法

在github上面已经有人提出此问题,并且标记为了bug,链接如下:https://github.com/spring-projects/spring-framework/issues/25681

并且修复pull request如下 (此代码已合并到v5.2.9.release分支)

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

4.1 办法1

升级spring版本到5.2.9.release+,可以彻底解决此问题

4.2 办法2

第一步在项目启动的时候,获取sqlerrorcodes,如果为空,则打印error日志并且告警。让开发同学知道有这么一个问题 (可重启也可不重启)

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 请重启服务");
          }
       } catch (exception e) {
          log.error("failed to preload database metadata", e);
       }
    }
}

第二步重新查询一遍数据库

如果有数据则表明是索引冲突,如果没有数据,则可能是其他异常引起的,走原有的老逻辑

catch (duplicatekeyexception e) {
    log.error("duplicatekeyexception e = {}", e.getmessage(), e);
}
catch (dataintegrityviolationexception e) {
    log.error("dataintegrityviolationexception e = {}", e.getmessage(), e);
    // 重新查一遍数据库,如果有数据,说明是唯一索引冲突
    person p = select(xxxx)
    if (p != null) {
        // 唯一索引冲突
    } else {
        // 其他异常引起的
    }
}
(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com