Spring @Transactional自调用失效:原理剖析与五种解决方案
2026/9/9 12:49:44 网站建设 项目流程

1. 问题先导:一个常见的"诡异"场景

先看一段代码。这是在业务系统里非常典型的写法,我敢说绝大部分Java开发都写过类似的:

@Service public class UserService { @Transactional(rollbackFor = Exception.class) public void createUserAndLog(String name) { userDao.insert(name); saveOperationLog(name); // 自调用:同类内部直接调用 } @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void saveOperationLog(String name) { logDao.insert("create user: " + name); throw new RuntimeException("记录日志失败"); } }

你满心以为:createUserAndLog开启事务,插入用户成功后,再开一个新事务记录日志,日志失败不影响用户插入,用户数据应该正常提交。

结果一跑,用户数据也回滚了。

还有一个更典型、更让新人懵的场景:

@Service public class UserService { public void doBiz() { updateUser(); } @Transactional public void updateUser() { userDao.update(...); throw new RuntimeException("触发回滚"); } }

doBiz方法从外部被调用,内部走updateUser()updateUser明明标了@Transactional,可异常抛出后数据库照样回滚了吗?

答案:没回滚。数据稳稳地写进去了。

这就是社区里被反复讨论的"自调用(self-invocation)导致 @Transactional 失效"问题。我在面试候选人的时候也经常拿这道题来摸底:能答到"这是AOP代理导致的"的人,大概占三成;能进一步讲清楚"为什么自调用绕过了代理对象"的人,十个人里面不一定有一个。这篇文章就把这件事彻底掰开揉碎讲清楚,从现象到原理,再到解决方案,最后附上我在实际项目里排查事务失效问题的一些经验。

2. 现象复现:自调用到底是怎么"失灵"的?

2.1 最小复现 Demo

先把复现环境交代清楚。我用的是 Spring Boot 2.7.x + MyBatis-Plus + MySQL,数据库表很简单,一张user表,一张operation_log表。为了项目直观,我把事务场景写进同一个 Service 里:

@Service public class DemoService { @Resource private UserMapper userMapper; @Resource private OperationLogMapper logMapper; /** * 外部调用入口 */ public void outerMethod() { System.out.println(">>> outerMethod 开始"); innerTxMethod(); System.out.println(">>> outerMethod 结束"); } /** * 标注了事务,但被同类内部方法直接调用 */ @Transactional(rollbackFor = Exception.class) public void innerTxMethod() { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); throw new RuntimeException("模拟业务异常"); } }

测试入口很简单:

@SpringBootTest class DemoServiceTest { @Resource private DemoService demoService; @Test void testSelfInvocation() { try { demoService.outerMethod(); } catch (Exception e) { System.out.println("捕获异常:" + e.getMessage()); } // 查一下数据库,看user表有没有数据 } }

执行结果:outerMethod调用innerTxMethodinnerTxMethod里抛了异常,事务注解按道理应该生效并回滚——但你去查user表,zhangsan这条数据已经老老实实躺在里面了。

换句话说:@Transactional在这个调用链上完全没起作用。

2.2 把"代理"三个字摆到台面上

为什么失效?网上的标准答案是:"因为自调用没有经过Spring的代理对象。"

这句话本身没错,但太笼统了,很多新手听完依然不知道问题出在哪。我换个角度解释:

Spring 的@Transactional不是靠改Java代码实现的,它是靠"在你不知道的地方,把你的 Bean 换成了一个代理对象"实现的。当 Spring 容器扫描到DemoService这个类里有@Transactional注解时,容器实际放进来的并不是DemoService这个类的直接实例,而是一个经过包装后的"代理对象"。

你从外部注入并调用demoService.outerMethod()时,调用的是这个代理对象的方法。代理对象在执行方法前后插入了一堆事务逻辑:开启事务、执行方法、异常回滚、正常提交。

但问题来了:outerMethod()方法内部写的this.innerTxMethod(),这个this指的是谁?

2.3 this 和 proxy 的根本区别

this指向的是当前这个对象实例。在代理模式下,this指向的恰恰是那个"没有被包装过的原始对象",而不是代理对象。

打个比方。你请了个保安(代理对象)在小区门口站岗,所有访客(外部调用)都必须经过保安登记(事务拦截)才能进楼。可是你这栋楼有内部通道,你自己从屋里直接走到隔壁办公室(自调用),保安根本看不见你——内部通道上可没设岗。

所以innerTxMethod上面的@Transactional注解虽然写着,但执行它的路径根本没经过"保安",事务拦截器压根儿没上班,自然就谈不上什么开启事务、异常回滚。

注意:这里我把"事务拦截器"比作保安,是为了方便理解。实际上 Spring 的事务逻辑藏在TransactionInterceptor里,后面第3部分会详细拆解这个拦截器的执行链路。

理解了"this是原始对象、外部调用是代理对象"之后,自调用失效的谜底就揭开一大半了。剩下要搞清楚的,是 Spring 究竟怎么把 Bean 变成代理对象的、以及事务拦截器到底在哪一步插入逻辑。

3. 底层原理:@Transactional 的代理机制与执行链路

3.1 @Transactional 的本质是 AOP 通知

Spring 的事务管理,底层依赖的是 AOP(面向切面编程)。

Spring AOP 的核心概念可以简化成三件事:

  • 切面(Aspect):横切关注点的模块化,比如"事务管理"就是一个切面。
  • 通知(Advice):切面在特定时机执行的逻辑,比如"开启事务""提交事务""回滚事务"。
  • 切点(Pointcut):判断哪些方法需要被切入。

@Transactional注解本身并不干活,它只是一个"标记"。真正干活的是一个叫TransactionInterceptor的类,它实现了事务通知的逻辑。Spring 启动时,会扫描所有 Bean 的方法,一旦发现某个方法或类上标了@Transactional,就把这个 Bean 包装成代理对象,并让TransactionInterceptor插入到方法调用链上。

可以理解为:代理对象在外层包了一个壳,壳里拦截到方法调用后,先执行事务逻辑,再执行真正的方法代码。

外部调用 ↓ 代理对象.method() ← 这里先被 TransactionInterceptor 拦住了 ↓ 事务逻辑(开启事务) ↓ 真实对象.method() ← 你的业务代码在这里执行 ↓ 事务逻辑(提交或回滚)

注意看:如果你从外部调demoService.outerMethod(),会完整走上面这条链路。但如果outerMethod()里写的是this.innerTxMethod()innerTxMethod方法的调用起点是"真实对象",根本没机会回到"代理对象"那里,链路就变成:

外部调用 ↓ 代理对象.outerMethod() ↓ 事务逻辑(outerMethod 有事务则开启) ↓ 真实对象.outerMethod() ↓ 真实对象.innerTxMethod() ← 直接从真实对象身上调用,不再经过代理

所以innerTxMethod上的@Transactional形同虚设。

3.2 Spring 如何决定创建哪种代理对象

这里引出一个很多人面试时经常被追问的点:JDK 动态代理和 CGLIB 代理的区别。

Spring AOP 默认支持两种代理方式:

第一种:JDK 动态代理

  • 要求目标类必须实现至少一个接口。
  • 基于java.lang.reflect.Proxy生成一个"接口的代理实现"。
  • 代理类和目标类是平级关系,都实现了同一个接口。
  • 优点:不需要额外引入依赖,基于 JDK 原生反射。
  • 缺点:只能代理接口中定义的方法;如果某个方法不在接口里,代理不了。

第二种:CGLIB 代理

  • 不要求目标类实现接口。
  • 基于字节码生成技术,直接生成目标类的一个子类。
  • 通过继承父类的方式,重写父类方法来实现拦截。
  • 优点:可以代理普通类,不需要接口。
  • 缺点:final类、final方法无法被 CGLIB 代理;JDK 9+ 需要额外的--add-opens参数(较新版本越来越省心,但老项目偶尔能碰到)。

在 Spring Boot 2.x+ 的默认配置下,spring.aop.proxy-target-class=true,也就是说默认走 CGLIB 代理。哪怕你的类实现了接口,Spring Boot 也默认用 CGLIB,只有在显式配置为false时才退回 JDK 动态代理。

那实际开发中怎么确认自己的 Bean 到底是 JDK 代理还是 CGLIB 代理?

最土的办法是在启动后打印一下对象类型:

System.out.println(demoService.getClass());

如果是 JDK 动态代理,打印结果里会有jdk.proxy或者com.sun.proxy字样;如果是 CGLIB,打印结果一般是com.example.service.DemoService$$EnhancerBySpringCGLIB$$xxxxx

注意:这个细节在排查"事务失效"问题时非常有用。有些场景下,你以为自己在调代理对象,实际上因为类型不对,Spring 注入的是别的对象,打印类的toString一眼就能看出问题。

3.3 TransactionInterceptor 的事务拦截逻辑

TransactionInterceptor是 Spring 事务通知的核心实现,它的工作流程可以用下面几步概括:

  1. 确定事务属性:解析方法或类上的@Transactional注解,拿到传播行为(propagation)、回滚规则(rollbackFor)、隔离级别(isolation)等配置。
  2. 判断是否已有事务:根据传播行为决定是新建事务、加入已有事务,还是挂起已有事务(如REQUIRES_NEW就表示挂起当前事务、新开一个)。
  3. 执行业务方法:通过反射调用目标方法。
  4. 方法正常返回:如果业务方法没有抛异常,提交事务。
  5. 方法抛出异常:根据回滚规则判断该不该回滚。默认情况下,只有RuntimeExceptionError才会触发回滚,Checked Exception(编译期异常)不会回滚。这也是一个高频踩坑点。

伪代码大致是这样:

// 这是 TransactionInterceptor 内部逻辑的简化示意 public Object invoke(MethodInvocation invocation) throws Throwable { TransactionInfo txInfo = createTransactionIfNecessary(...); Object result; try { result = invocation.proceed(); // 调用真正的业务方法 } catch (Throwable ex) { completeTransactionAfterThrowing(txInfo, ex); // 根据规则回滚或提交 throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); // 正常返回时提交 return result; }

理解了这段逻辑,你就能解释很多"诡异"现象:

  • 为什么@Transactional方法在同类里被this.xxx()调用会失效?——因为走了原始对象,TransactionInterceptor根本没被触发。
  • 为什么在事务方法里 catch 掉异常就不会回滚?——因为TransactionInterceptor根本没看到业务方法抛出的异常,它只看到方法正常返回。
  • 为什么Checked Exception默认不回滚?——因为默认回滚规则只匹配RuntimeExceptionError,要想让编译期异常也回滚,必须显式写rollbackFor = Exception.class

3.4 代理创建的底层入口:BeanPostProcessor

可能有人要问:Spring 到底在什么时机、哪个环节把普通 Bean 替换成代理对象的?

答案是:在 Bean 的生命周期里,有一个叫BeanPostProcessor的后置处理器扩展点。Spring 在 Bean 初始化完成之后,会调用所有的BeanPostProcessor,其中有一个专门负责处理 AOP 的后置处理器,叫做AbstractAutoProxyCreator

它的工作流程大概是:

  1. 扫描容器里所有的Advisor(通知器,包含了切点和通知)。
  2. 判断当前这个 Bean 是否匹配某个Advisor的切点。
  3. 如果匹配,就用ProxyFactory创建代理对象。
  4. 把代理对象返回给容器,后续注入到其他 Bean 里的就是这个代理对象。

对于@Transactional来说,Spring 内置了一个BeanFactoryTransactionAttributeSourceAdvisor,这个 Advisor 的切点会去解析方法上有没有@Transactional注解,通知就是TransactionInterceptor

所以整体链路就是:

@Transactional 注解 ↓ BeanFactoryTransactionAttributeSourceAdvisor 识别 ↓ AbstractAutoProxyCreator 创建代理 ↓ 代理对象内部织入 TransactionInterceptor ↓ 事务逻辑在方法调用前后生效

这个链路不需要背源码,但理解它之后,你再回头去看"为什么自调用会失效",就不是死记结论,而是能从机制上推导出来。

4. 五种解决自调用失效的方案

4.1 方案一:把事务方法拆到独立的 Bean

既然自调用失效的本质是"绕过代理对象",那最直接、最干净的办法就是……不让它自调用。

把需要事务的方法放到另一个 Service/Component 里,从外部调用:

@Service public class DemoService { @Resource private InnerTxService innerTxService; public void outerMethod() { System.out.println(">>> outerMethod 开始"); innerTxService.innerTxMethod(); System.out.println(">>> outerMethod 结束"); } } @Service public class InnerTxService { @Transactional(rollbackFor = Exception.class) public void innerTxMethod() { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); throw new RuntimeException("模拟业务异常"); } }

这时候DemoService.outerMethod()内部调用的是InnerTxService的代理对象,事务自然生效。

为什么我把这个方案排在最前面?因为它在团队协作中最不容易出错:

  • 可读性好:事务边界被单独拎出来,看代码的人一眼就知道哪个方法有事务。
  • 职责清晰:不同的事务逻辑放到不同的类,符合单一职责。
  • 不依赖魔法配置:不像AopContext.currentProxy()那样需要额外开启配置,也不存在@Resource注入自己是否循环依赖的顾虑。
  • 容易测试:可以单独 mockInnerTxService,也可以对DemoService做单元测试。

缺点也很明显:类数量会变多,小项目可能觉得"为了一个方法单独建类"太啰嗦。

我的建议是:在一个团队里定个约定,凡是事务方法尽量放到独立类中,或者至少不要写成"同类内互相调用事务方法"的形态。这个约定能帮你砍掉一大部分隐性 bug。

4.2 方案二:注入一个"自己"的代理

这个思路也很直白:既然this指向原始对象,那我就注入一个"代理对象"进来,通过代理对象调用事务方法。

@Service public class DemoService { // 注入自己 @Resource private DemoService self; public void outerMethod() { System.out.println(">>> outerMethod 开始"); self.innerTxMethod(); System.out.println(">>> outerMethod 结束"); } @Transactional(rollbackFor = Exception.class) public void innerTxMethod() { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); throw new RuntimeException("模拟业务异常"); } }

注意,self注入的不是原始对象,而是经过 Spring 包装的代理对象,所以self.innerTxMethod()能正常触发事务。

使用上有几个细节要提醒:

  • 优先用@Resource或者@Autowired字段注入。如果用构造器注入,在 Spring 2.6+ 的循环依赖默认不允许的配置下,可能会出现启动报错,因为DemoService的构造器里要注入DemoService自己。字段注入一般能绕开这个坑,因为字段注入是 Bean 创建完成后再注入的,不存在构造器循环依赖。
  • @Lazy是更稳的做法@Resource @Lazy private DemoService self;。延迟注入可以进一步避免循环依赖问题。
  • 注意方法访问权限:如果innerTxMethodprivate,哪怕通过代理对象调也调不到(Spring 代理无法拦截私有方法),一定要保证它是public的。

优点是不用额外建类,改动量小。缺点是"注入自己"这个写法对没接触过的人来说有点绕,而且如果团队里有人不理解原理,可能会当成"死循环注入"给改掉。

4.3 方案三:利用 AopContext.currentProxy()

Spring 提供了一个工具类AopContext,可以拿到当前对象的代理对象。用法:

@Service public class DemoService { public void outerMethod() { System.out.println(">>> outerMethod 开始"); // 从 AopContext 中取当前代理对象 DemoService proxy = (DemoService) AopContext.currentProxy(); proxy.innerTxMethod(); System.out.println(">>> outerMethod 结束"); } @Transactional(rollbackFor = Exception.class) public void innerTxMethod() { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); throw new RuntimeException("模拟业务异常"); } }

有一个前置条件必须做:在配置类上显式开启暴露代理对象:

@Configuration @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) public class AppConfig { }

如果你的项目用了 Spring Boot,默认配置下需要自己声明一下@EnableAspectJAutoProxy(exposeProxy = true),否则运行时会抛IllegalStateException: Cannot find current proxy

这个方案的好处是改动范围集中在方法内部,不用新增类。坑也比较明显:

  • 必须开exposeProxy,全局开启暴露代理会有一点点性能损耗(其实微乎其微,但洁癖型架构师会介意)。
  • 强转类型容易踩雷:如果开启了proxyTargetClass=truecurrentProxy()返回的是 CGLIB 代理,强转成DemoService没问题;但如果 JVM 因为某些原因回退到了 JDK 动态代理,强转就会ClassCastException
  • 代码可读性一般,团队新人看到AopContext.currentProxy()第一反应通常是"这是什么黑魔法"。

所以说,能用方案一就别用方案三。这个方案适合那种"代码结构实在不好动、又急着修 bug"的场景。

4.4 方案四:从容器里手动取代理对象

思路和方案二类似,只是"拿代理对象"的方式从注入改成了主动获取:

@Service public class DemoService implements ApplicationContextAware { private ApplicationContext applicationContext; public void outerMethod() { System.out.println(">>> outerMethod 开始"); DemoService proxy = applicationContext.getBean(DemoService.class); proxy.innerTxMethod(); System.out.println(">>> outerMethod 结束"); } @Transactional(rollbackFor = Exception.class) public void innerTxMethod() { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); throw new RuntimeException("模拟业务异常"); } @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } }

applicationContext.getBean(DemoService.class)拿到的必然是 Spring 容器里的代理对象(如果这个 Bean 被代理了的话),所以innerTxMethod的事务能生效。

这个方案相当于手动模拟了 Spring 的依赖注入,能用但不优雅。它比较适合那些"注册表模式""策略模式"的场景——方法所属的 Bean 是在运行时才决定用什么类型的。

实际项目里我很少推荐这个,因为你完全可以在一开始就把self注入进来,没必要每次调用都去容器查一次。

4.5 方案五:绕开注解,用编程式事务

有时候,与其纠结注解失效的问题,不如换个思路:直接用编程式事务,把事务边界显式写进代码里。

Spring 提供了TransactionTemplate,用它可以在任意地方开启事务:

@Service public class DemoService { @Resource private TransactionTemplate transactionTemplate; public void outerMethod() { System.out.println(">>> outerMethod 开始"); transactionTemplate.execute(status -> { try { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); throw new RuntimeException("模拟业务异常"); } catch (RuntimeException e) { // 可在这里设置回滚状态 status.setRollbackOnly(); throw e; } }); System.out.println(">>> outerMethod 结束"); } }

也可以用PlatformTransactionManager手动控制:

@Service public class DemoService { @Resource private PlatformTransactionManager transactionManager; public void outerMethod() { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); def.setTimeout(30); TransactionStatus status = transactionManager.getTransaction(def); try { userMapper.insert(new User("zhangsan")); logMapper.insert(new OperationLog("insert user zhangsan")); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } } }

编程式事务最大的优点是精确可控:没有代理、没有 AOP、没有"自调用失效"的问题,事务边界一目了然。缺点也很明显:

  • 代码侵入性比注解式事务强得多,业务代码里塞了事务开启/提交/回滚逻辑,可读性受一点影响。
  • TransactionTemplate的回调写法需要小心setRollbackOnly和抛出异常的配合,不然容易出现"虽然设置了回滚但事务管理器没收到通知"的问题。

我个人一般这么选:常规业务用@Transactional+ 拆 Bean;复杂的、需要动态控制事务边界的场景(比如循环内处理多条数据、重试后决定是否回滚、跨方法组合事务),宁可上TransactionTemplate,也别硬凑注解。

4.6 五种方案对比

方案改动量可读性适用范围踩坑点
拆独立 Bean中等通用,推荐优先类数量变多
注入自己快速修复构造器循环依赖;private 方法无效
AopContext.currentProxy()较差结构不好动时救急必须开启 exposeProxy
从容器取代理动态获取场景与 Spring 容器耦合
编程式事务复杂事务边界代码侵入性高

5. 事务失效排查经验:别光盯着"自调用"

5.1 先确认代理到底存不存在

遇到事务不生效,第一步不要急着查数据库,先打印一下你调用的对象到底是什么类型:

System.out.println(this.demoService.getClass());

我帮同事排查问题时,经常看到有人判断" @Transactional 失效",结果打印出来发现:

class com.example.service.DemoService

$$EnhancerBySpringCGLIB都没有——说明这个 Bean 压根没有被代理,根本不是自调用的问题,而是这个类根本不在 Spring 容器里(比如用new出来的),或者代理创建被某些配置关闭了。

所以我的排查顺序是:

  1. 打印对象getClass(),确认是代理类。
  2. 如果连代理类都不是,检查类上是否漏了@Service/@Component,或者是不是用new直接创建了。
  3. 如果是代理类,但事务还是失效,检查是不是自调用、是不是private方法、是不是在catch里吞了异常、是不是抛了Checked Exception但没配rollbackFor
  4. 看日志,确认有没有经过事务拦截器(见 5.3)。

5.2 事务失效常见原因速查表

这里总结一份我多年踩坑整理出来的速查表,基本覆盖日常开发里能见到的绝大部分情况:

失效原因示例排查方式
同类自调用this.innerTxMethod()看调用方式,打印代理类
方法非 publicprivate void txMethod()Spring 代理无法拦截 private
类被 final 修饰final class DemoServiceCGLIB 无法继承 final 类
异常被吞掉try-catch后没抛出事务拦截器认为方法正常返回
Checked Exception 无 rollbackForIOException默认只回滚RuntimeExceptionError
类未被 Spring 管理手动new DemoService()检查 Bean 注解
绕过代理直接调用target.method()不是注入对象检查调用路径
多线程子线程里调事务方法事务绑定在当前线程的数据库连接上
事务传播配置理解错误REQUIRES_NEW参与方以为是独立事务核对日志确认事务 ID

5.3 用日志确认事务边界

Spring 的事务框架本身带有非常详细的日志输出,可以在application.yml里打开:

logging: level: org.springframework.transaction: TRACE org.springframework.jdbc.datasource: DEBUG

跑一次之后,你会看到类似下面的日志:

Getting transaction for [com.example.service.DemoService.innerTxMethod] Participating in existing transaction Completing transaction for [com.example.service.DemoService.innerTxMethod] Releasing JDBC Connection after transaction

如果看到Getting transaction for ...,说明事务拦截器确实介入了。如果方法里抛了异常,日志里还会出现:

Initiating transaction rollback Rolling back JDBC transaction on Connection ...

要是你的方法全程连Getting transaction for都没出现,那就是事务拦截器压根没执行到,这时候要回到代理和调用链路上查。

5.4 设计事务方法的三条经验

这些经验来自项目实践,不一定写在文档里,但很实用:

经验一:事务方法要"短、平、快"。不要在事务里做远程调用、RPC、消息推送、大文件读写。事务的本质是"锁住数据库资源",事务时间越长,连接占用越久,锁冲突概率越高。有些线上事故就是事务里调了超时的外部接口,导致连接池被占满。

经验二:事务边界要尽量和"业务边界"对齐。一个事务只做一件事。不要把多个独立操作硬塞进一个大事务里,回头又用REQUIRES_NEW切来切去,这样排查问题时心态会崩。

经验三:培养"一看代码就知道事务会不会失效"的判断力。看到this.xxx(),脑子里立刻弹警报警告;看到private方法上有@Transactional,赶紧把它改public;看到try-catch里吞异常,想一下事务回滚的条件。这种条件反射比背一百个面试题都顶用。

6. 写在最后的几点补充

这个话题在 Spring 社区里属于"老生常谈",但常在河边走哪有不湿鞋,我见过不少工作四五年的开发在代码 review 时,依然会给同一个类里的方法互相调用加上@Transactional,然后理直气壮地告诉我说"这样应该没问题吧"。

每次遇到这种情况,我都建议对方先去把当前对象的getClass()打出来看一眼。你亲眼见过$$EnhancerBySpringCGLIB$$这个后缀,再理解"代理对象和真实对象不是同一个东西",自调用失效这件事就能在脑子里扎下根。

另外多说一句,这类问题在团队中最好沉淀成一份简单的规范,比如:

  • @Transactional只允许写在 public 方法上;
  • 事务方法尽量不要在同类里被其他方法调用;
  • 一定要自调用时,优先考虑拆独立 Bean;
  • 事务方法内部不要 catch 掉异常后不重新抛出;
  • 需要编译期异常回滚的,显式配置rollbackFor = Exception.class

这些约定不复杂,但能省下大量排查事务诡异行为的时间。顺便说一句,Spring 的代理原理不只是@Transactional的根基,它也是理解@Async@Cacheable@Retryable这类注解的通用钥匙。你把这一套代理机制吃透了,再遇到"为什么我的 @Async 不生效""为什么 @Cacheable 缓存没走"这类问题,大概率能举一反三,不用网上搜半天。

我在实际开发里最常用的方案还是拆 Bean,虽然多一个类,但换来的是明确的事务边界和更顺畅的 review 体验。如果你也在项目里碰到了类似的问题,不妨先打开代码跑一把getClass(),再对照这篇文里的排查表走一遍——我赌十有八九能直接定位到原因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询