当前位置: 首页 > 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.jsqmd.com/news/1107363/

相关文章:

  • 终极隐私保护神器:Boss-Key老板键一键隐藏Windows窗口完整指南
  • MeEdu开源教育系统:如何构建多云协同的视频点播架构
  • OptiStruct自从有了NVHD,整车NVH分析so easy
  • IAP升级方案
  • linux 安装达梦数据库
  • npm 包开发避坑指南:Scope 命名空间管理的 4 种常见错误与修复方案
  • KeyStore Explorer:为什么Java开发者需要告别keytool命令行的五个理由
  • AI + 智能客服系统完整设计方案
  • ONNX模型解析与优化实战指南
  • Jmeter基础知识详解
  • Linux无线网卡兼容性难题:RTL8821CU驱动深度配置指南
  • 电子系统散热管理:从芯片级到系统级的优化策略
  • 2026进口闸阀品牌排行榜
  • 计算机毕业设计之河北经贸大学毕业生就业跟踪系统
  • Agent工作流编排的“可控性”难题:SwarmFlow的解决方案
  • 如何在Windows和Mac电脑上录制特定窗口
  • GitHub Copilot × IDEA效率黑盒拆解(仅限内部技术团队流通的LLM token调度策略)
  • Krita Vision Tools深度解析:AI智能选区工具的创新应用实战指南
  • 铜钟音乐:5分钟掌握纯净无干扰的免费听歌平台终极指南
  • Redis 连接失败对网站的影响:何时该先测网络再查缓存
  • KMX63与PIC18F87J10实现低成本自然交互方案
  • 从工具到思维:2025年,AI模型如何重写产业规则?
  • MANO手部模型完整指南:从零开始构建3D手部动画
  • 终极隐私保护方案:Boss-Key老板键一键隐藏Windows窗口的完整教程
  • 大型项目验证的企业级 Vue3 项目架构模板 从零到生产可用的架构骨架
  • 从新手到Expert:IDEA项目导入报错响应时效对比——手动排查需47分钟 vs 自动化脚本仅8.3秒(实测数据+可复现Demo)
  • 如何用Resynthesizer插件实现专业级图像修复与纹理合成:GIMP用户的终极指南
  • MeEdu开源网校系统:如何构建高可用、低成本的视频点播平台架构
  • Nexus 搭建 npm 私有仓库:3 步配置 npm-proxy 代理与 5 个包发布避坑要点
  • Android视频播放终极指南:5种比例模式适配告别黑边烦恼