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 {
// 其他异常引起的
}
}

Spring异常处理 bug !!!同一份代码,结果却不一样?的更多相关文章

  1. Spring异常处理@ExceptionHandler

    最近学习Spring时,认识到Spring异常处理的强大.之前处理工程异常,代码中最常见的就是try-catch-finally,有时一个try,多个catch,覆盖了核心业务逻辑: try{ ... ...

  2. Unit06: Spring对JDBC的 整合支持 、 Spring+JDBC Template、Spring异常处理

    Unit06: Spring对JDBC的 整合支持 . Spring+JDBC Template .Spring异常处理 1. springmvc提供的异常处理机制 我们可以将异常抛给spring框架 ...

  3. spring boot 和shiro的代码实战demo

    spring boot和shiro的代码实战 首先说明一下,这里不是基础教程,需要有一定的shiro知识,随便百度一下,都能找到很多的博客叫你基础,所以这里我只给出代码. 官方文档:http://sh ...

  4. [转载]基于TFS实践敏捷-修复Bug和执行代码评审

    本主题阐释了这些功能,以继续这一关注虚拟敏捷团队成员的一天的教程. Peter 忙于编写一些代码以完成积压工作 (backlog) 项任务.但是,他的同事发现了一个阻碍他们工作的 Bug,他想立即修复 ...

  5. Spring框架的反序列化远程代码执行漏洞分析(转)

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

  6. Spring进阶—如何用Java代码实现邮件发送(一)

    相关文章: <Spring进阶—如何用Java代码实现邮件发送(二)> 在一些项目里面如进销存系统,对一些库存不足发出预警提示消息,招聘网站注册用户验证email地址等都需要用到邮件发送技 ...

  7. 淘系工程师讲解的使用Spring特性优雅书写业务代码

    使用Spring特性优雅书写业务代码   大家在日常业务开发工作中相信多多少少遇到过下面这样的几个场景: 当某一个特定事件或动作发生以后,需要执行很多联动动作,如果串行去执行的话太耗时,如果引入消息中 ...

  8. spring异常处理

    http://cgs1999.iteye.com/blog/1547197 1 描述 在J2EE项目的开发中,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到 ...

  9. Spring 异常处理三种方式 @ExceptionHandler

    异常处理方式一. @ExceptionHandler 异常处理方式二. 实现HandlerExceptionResolver接口 异常处理方式三. @ControllerAdvice+@Excepti ...

  10. 用好spring mvc validator可以简化代码

    表单的数据检验对一个程序来讲非常重要,因为对于客户端的数据不能完全信任,常规的检验类型有: 参数为空,根据不同的业务规定要求表单项是必填项 参数值的有效性,比如产品的价格,一定不能是负数 多个表单项组 ...

随机推荐

  1. Blazor Hybrid适配到HarmonyOS系统

    1. 前言 Blazor Hybrid是一个基于Web技术的MVVM开发模式的客户端框架,它只有UI是由Webview渲染,而逻辑均由.NET开发,可以突破浏览器限制访问本地文件,或者发起TCP或者U ...

  2. 基于标签值分布的强化学习推荐算法(Reinforcement Learning Recommendation Algorithm Based on Label Value Distribution)

    前言 看论文的第三天,坚持下去. 慢慢来,比较快. -- 唐迟 本文基于2023年6月28日发表在MATHEMATICS上的一篇名为"基于标签值分布的强化学习推荐算法"(Reinf ...

  3. Windows 提权-UAC 绕过

    本文通过 Google 翻译 UAC-Bypass – Windows Privilege Escalation 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释补充. 导航 ...

  4. 在 Intel Ultra AI PC 设备上使用 OpenVINO™ C# API本地部署YOLOv11与YOLOv12

    ​ 最新的英特尔 酷睿 Ultra 处理器(第二代)让我们能够在台式机.移动设备和边缘中实现大多数 AI 体验,将 AI 加速提升到新水平,在 AI 时代为边缘计算提供动力.英特尔 酷睿 Ultra ...

  5. 【由技及道】镜像圣殿建造指南:Harbor私有仓库的量子封装艺术【人工智障AI2077的开发日志009】

    摘要:当容器镜像需要同时存在于8个平行宇宙时,就像在量子计算机里管理72个维度的镜像分身.本文记录一个未来AI如何通过Harbor搭建量子镜像圣殿,让容器分发成为跨越时空的瞬间传送. 动机:镜像管理的 ...

  6. PanWeiDB2.0异构数据库访问测试

    PanWeiDB2.0异构数据库访问测试 异构数据库访问兼容性测试一览表 No 访问路径 多维度结果 备注 1 PanWeiDB(集中式)-访问-PanWeiDB(集中式) √ 支持复杂SQL 2 P ...

  7. 【记录】C/C++-关于I/O的坑与教训

    吐槽 每每读取字符串时,倘若稍有灵活的操作,总会遇上诡异奇怪的事情.究其原因,就是没完全理解一些基本读写函数的机制.这次做Uva227就把I/O上的问题全暴露出来了.想来还是应该记录一些经验教训. 记 ...

  8. 后台返回文件URL,前端下载文件,即使设置文件名,下载的文件名称并不是自己所设置的问题

    1.背景 项目中遇到这么一个问题,上传文件后,后台返回的是一个URL,前端需要通过点击下载这个文件 2.首次处理 当时一看是下载文件,觉得很简单,无非是通过创建a标签来实现,以下是我当时的代码,用的是 ...

  9. 深入解析 Druid 连接池:连接有效性检测与 Keep-Alive 机制

    背景 在 Java 程序中,下面是一个经常会碰到的错误. Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communica ...

  10. python正则表达式笔记2

    由 '\' 和一个字符组成的特殊序列在以下列出. 如果普通字符不是ASCII数位或者ASCII字母,那么正则样式将匹配第二个字符.比如,\$ 匹配字符 '$'. \number匹配数字代表的组合.每个 ...