news 2026/7/2 7:06:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
踩坑总结: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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 7:03:41

电子系统散热管理:从芯片级到系统级的优化策略

1. 为什么电子系统需要主动散热管理?在汽车电子和工业控制领域,散热管理一直是系统可靠性的关键瓶颈。以我参与过的某车载ECU项目为例,当环境温度达到45℃时,未优化散热的PCB板温度会在30分钟内飙升到85℃以上,直接导致…

作者头像 李华
网站建设 2026/7/2 7:03:20

js自定义Emitter,实现1对多的事件派发处理

增加监听on、派发事件emit、移除指定监听off、移除所有监听removeAllListenners<!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><script>class SimpleEventEmitter {/*** 存…

作者头像 李华
网站建设 2026/7/2 7:02:05

多链路聚合:无人机电力巡检的“超视距生命线”

多链路聚合&#xff1a;无人机电力巡检的“超视距生命线”传统电力巡检中&#xff0c;无人机常面临山区信号盲区、变电站强电磁干扰、高空基站切换卡顿等难题&#xff0c;导致图传中断、“盲飞”风险高&#xff0c;4K高清及红外数据无法实时回传。多链路聚合设备通过融合多运营…

作者头像 李华
网站建设 2026/7/2 7:00:33

深度解析:Cursor编辑器系统配置优化与开发环境稳定化架构设计

深度解析&#xff1a;Cursor编辑器系统配置优化与开发环境稳定化架构设计 【免费下载链接】go-cursor-help 解决Cursor在免费订阅期间出现以下提示的问题: Your request has been blocked as our system has detected suspicious activity / Youve reached your trial request …

作者头像 李华
网站建设 2026/7/2 7:00:17

计算机Java毕设实战-基于 SpringBoot 的应急储备物资仓储管理系统的设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/7/2 6:59:51

实现知识无缝迁移:Obsidian Importer多平台笔记导入解决方案

实现知识无缝迁移&#xff1a;Obsidian Importer多平台笔记导入解决方案 【免费下载链接】obsidian-importer Convert your data to Markdown files you can use in Obsidian. Works with Apple Notes, OneNote, Evernote, Notion, Google Keep, and many other formats. 项目…

作者头像 李华