当前位置: 首页 > news >正文

踩坑总结:Spring @Transactional 事务注解的这几个坑,你踩过几个?

前言

最近在做项目的时候,又碰到了@Transactional事务失效的问题。说实话,这个注解看似简单,但用不好真的能把人坑惨。今天就把我踩过的几个坑整理出来,都是实战中实打实遇到的问题,希望能帮大家少走点弯路。


坑一:自己注入自己?小心循环依赖

先问大家一个问题:在 Service 里自己注入自己,会不会出现循环依赖?

@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@AutowiredprivateUserInfoServiceImpluserInfoService;// 自己注入自己// ...}

很多人第一反应是:肯定会啊!但实际上,Spring 原生是支持解决循环依赖的,靠的就是那三级缓存。

但是!重点来了 ——Spring Boot 默认把循环依赖给关了

对,你没听错。Spring Boot 2.6 之后,默认是不支持循环依赖的,启动直接给你报BeanCurrentlyInCreationException

解决方案

如果你确实需要自己注入自己(后面会讲为什么需要这么做),可以在配置文件里把这个开关打开:

spring:main:allow-circular-references:true# 开启循环依赖支持

加上这个配置,循环依赖的问题就解决了。


坑二:同类方法调用,事务直接失效!(重点)

这个是最常见的坑,没有之一。

问题场景

假设你有一个 Service 类,里面有两个方法:

@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@TransactionalpublicvoidsaveUser(UserInfouser){// 操作用户主表userInfoMapper.insert(user);// 调用同类的另一个方法saveUserStatus(user.getId());// 这里有坑!}@TransactionalpublicvoidsaveUserStatus(LonguserId){// 操作用户状态附表userStatusMapper.insert(userId);}}

看起来没毛病对吧?两个方法都加了@Transactional,应该都在事务里啊?

大错特错!

UsersaveUserStatus()这个方法的事务根本不会生效!saveUserStatus()这个方法的事务根本不会生效!

为什么会失效?

原因很简单:Spring 的事务是基于 AOP 动态代理实现的。

  • 外部调用saveUser()时,实际上调用的是代理对象的方法,代理对象会帮你开启事务

  • 但在方法内部调用saveUserStatus()时,用的是this(也就是原始对象),不是代理对象

  • 没有经过代理对象,AOP 就拦不住,事务自然就失效了

怎么判断事务有没有生效?

教大家一个简单的判断方法:只要是this.方法名()调用的,事务注解都不生效

因为this代表的是当前对象本身,不是 Spring 生成的代理对象。


三种解决方案

方案一:抽到另一个 Service 里(最稳妥)

UsersaveUserStatus()抽到一个新的 Service 中:把saveUserStatus()抽到一个新的 Service 中:

@ServicepublicclassUserOperateServiceImplimplementsUserOperateService{@AutowiredprivateUserStatusMapperuserStatusMapper;@Override@TransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}

然后在原来的 Service 中注入这个新 Service:

@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@AutowiredprivateUserOperateServiceuserOperateService;@TransactionalpublicvoidsaveUser(UserInfouser){userInfoMapper.insert(user);userOperateService.saveUserStatus(user.getId());// 通过代理对象调用,事务生效}}

优点:最规范,没有任何副作用
缺点:要多写一个类,有点麻烦


方案二:自己注入自己
@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@AutowiredprivateUserInfoServiceuserInfoService;// 注入自己(用接口类型)@TransactionalpublicvoidsaveUser(UserInfouser){userInfoMapper.insert(user);userInfoService.saveUserStatus(user.getId());// 通过注入的代理对象调用}@TransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}

原理:注入的userInfoService是 Spring 生成的代理对象,通过它调用方法就能走 AOP。

注意:这种方式需要开启循环依赖支持,就是前面说的spring.main.allow-circular-references=true

优点:不用新建类,代码改动小
缺点:需要开启循环依赖,有的人可能觉得不优雅


方案三:用 AopContext 获取代理对象(个人推荐)

这是我最喜欢的方式,代码最简洁。

第一步:在启动类或配置类上加注解,暴露代理对象:

@SpringBootApplication@EnableAspectJAutoProxy(exposeProxy=true)// 关键:暴露代理对象publicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}

第二步:在方法中通过AopContext获取当前代理对象:

@ServicepublicclassUserInfoServiceImplimplementsUserInfoService{@TransactionalpublicvoidsaveUser(UserInfouser){userInfoMapper.insert(user);// 获取当前代理对象UserInfoServiceproxy=(UserInfoService)AopContext.currentProxy();proxy.saveUserStatus(user.getId());// 通过代理对象调用}@TransactionalpublicvoidsaveUserStatus(LonguserId){userStatusMapper.insert(userId);}}

优点:不用新建类,不用自己注入自己,代码清晰
缺点:需要加一个启动类注解

💡个人建议:优先用方案三,最优雅也最方便。如果项目规范要求不能这么写,再考虑方案一。


坑三:抛了异常,事务居然不回滚?

这个坑也超级常见!

问题场景

@TransactionalpublicvoidsaveUser(UserInfouser)throwsSQLException{userInfoMapper.insert(user);// 模拟抛出数据库异常if(user.getId()==null){thrownewSQLException("数据库异常");}}

你觉得上面的代码,抛了SQLException之后事务会回滚吗?

答案是:不会!

为什么不回滚?

因为 Spring 事务默认只对RuntimeExceptionError进行回滚。

来看看异常的继承关系:

Throwable ├── Error(Spring会回滚) └── Exception ├── RuntimeException(Spring会回滚) └── 其他Exception(比如SQLException,Spring不回滚!)

SQLException继承的是Exception,不是RuntimeException,所以 Spring 默认不回滚。

解决方案

加上rollbackFor属性,指定回滚的异常类型:

@Transactional(rollbackFor=Exception.class)// 所有Exception都回滚publicvoidsaveUser(UserInfouser)throwsSQLException{// ...}

这样只要是Exception及其子类的异常,都会触发事务回滚。

💡最佳实践:建议大家写@Transactional的时候,习惯性加上rollbackFor = Exception.class,避免踩坑。


补充:还有一个小细节

不知道大家注意到没有,IDEA 会在private方法上的@Transactional标红提醒。

为什么?因为事务注解必须加在public方法上。

私有方法外部访问不到,Spring 的代理也没法拦截,加了事务注解也没用。IDEA 很贴心地给你提示了。


总结

今天讲了@Transactional的三个大坑:

原因解决方案
循环依赖Spring Boot 默认关闭循环依赖配置spring.main.allow-circular-references=true
同类方法调用事务失效this调用,没走代理对象1️⃣ 抽到另一个 Service
2️⃣ 自己注入自己
3️⃣AopContext.currentProxy()(推荐)
异常不回滚默认只回滚RuntimeException加上rollbackFor = Exception.class

希望这篇文章能帮大家避避坑。如果觉得有用,点个赞收藏一下,以后遇到事务问题翻出来看看就行~

http://www.gsyq.cn/news/1619336.html

相关文章:

  • MeEdu开源教育系统:如何构建多云协同的视频点播架构
  • KeyStore Explorer:为什么Java开发者需要告别keytool命令行的五个理由
  • 终极隐私保护神器:Boss-Key老板键一键隐藏Windows窗口完整指南
  • 计算机毕业设计之河北经贸大学毕业生就业跟踪系统
  • 如何在Windows和Mac电脑上录制特定窗口
  • 铜钟音乐:5分钟掌握纯净无干扰的免费听歌平台终极指南
  • KMX63与PIC18F87J10实现低成本自然交互方案
  • Redis 连接失败对网站的影响:何时该先测网络再查缓存
  • 从工具到思维:2025年,AI模型如何重写产业规则?
  • 3步搞定视频下载难题:Parabolic让你的下载体验焕然一新
  • 我说MySQL每张表最好不超过2000万条数据,面试官让我回去等通知?
  • AI大模型到底改变了什么?这5个真相你必须知道
  • 无人机航拍垃圾识别数据集与模型训练实战
  • 基于LENA-R8与STM32的全球物联网高精度定位方案
  • 3.0 java中继承中的super作用
  • 深海定点监测如何选稳定单点海流计?偶信产品适配复杂海域工况吗?
  • Ubuntu系统Postgres SQL数据库迁移
  • 3步掌握Parabolic视频下载器:免费开源跨平台下载解决方案完全指南
  • VSCode 多项目协同开发:AI编程工具工作区管理的 4 种高效配置方案
  • 西安养老预约小程序搭建,家属实时查看功能开发实操
  • 大模型微调实战:解决过拟合与收敛慢的优化策略
  • 终极指南:如何用GTA5线上小助手提升你的游戏体验
  • CSDN博客下载器终极指南:三步永久保存技术文章
  • 初识C语言——lesson002与003
  • 1971–2025年三大生态功能数据集|水源涵养/防风固沙/土壤保持|20+生态价值指标|30m/250m逐年栅格
  • AI获客培训常见误区:从风口焦虑到长期运营
  • 终极图像修复指南:如何用Resynthesizer让GIMP拥有AI级图像处理能力
  • OpenHarmony dsoftbus快速入门:3步搭建你的第一个分布式应用
  • 第06篇:Transformer 解剖——Decoder-only 是怎么炼成的
  • Docker持续集成实践