
本文摘要Service 自调用同类方法时Transactional静默失效异常抛出后数据仍落库。3 行代码复现并解释代理被跳过的机制同时对比五种修复方案。一、问题与结论先说结论Spring 的Transactional不是注解自带的能力而是由Spring AOP 代理在方法调用入口拦截后才创建事务边界。当同一个 Bean 的方法 A 内部直接调用方法 B 时执行的是this目标对象而非代理对象事务拦截器完全不被触发。这类缺陷不会抛出任何警告属于静默失效——团队往往依赖Transactional保证数据一致性但由于自调用绕过了代理写入操作以 auto-commit 方式落库异常抛出后不会触发任何回滚。Spring Framework 5.x/6.x对应 Spring Boot 2.x/3.x中这一行为完全一致不属于版本缺陷而是 AOP 代理机制的固有设计。二、排查与选择依据排查此类问题关键在于确认方法调用是否经过代理对象。可通过以下线索判断检查调用路径如果Transactional标注的方法是通过同类中的另一个方法内部调用的即this.method()或直接method()代理链被跳过。检查异常是否被 catch 吞掉即使调用经过代理如果异常在Transactional方法内部被try-catch捕获且未重新抛出事务也不会回滚。检查方法访问级别private和protected方法上的Transactional在 CGLIB 模式下同样不生效Spring 启动时会打印 WARNING 日志。替代方案与取舍方案选择条件代价边界提取独立 Bean事务方法有明确业务边界需新增类文件拆分类结构推荐首选无额外配置长期可维护自注入Lazy快速修复不想改动 Bean 结构新成员不易理解自注入模式仅适合 1-2 个方法需要外部化调用的场景TransactionTemplate需要条件性回滚、部分提交等细粒度控制代码量增加事务边界显式化适合复杂事务逻辑不适合简单 CRUDAopContext.currentProxy()已有项目无法修改 Bean 结构时应急修复需全局开启spring.aop.expose-proxytrue强耦合 AOP 实现类型不安全不推荐在新项目使用AspectJ 编译期织入大量自调用无法重构需修改构建配置引入 AspectJ 依赖几乎不推荐绝大多数项目不需要如果事务方法只是简单 CRUD且没有复杂的传播行为需求提取独立 Bean 是最直接、最符合 Spring 惯例的做法。三、关键原理Spring 的Transactional通过TransactionInterceptor一个 AOP Around Advice在代理层拦截方法调用此时才创建事务并绑定到当前线程。Spring Boot 默认使用 CGLIB 类代理spring.aop.proxy-target-classtrue。CGLIB 通过生成目标类的子类实现拦截——子类重写的方法内部委托给TransactionInterceptor。关键在于this引用的指向publicvoidprocess(){this.saveAndFail();// this 指向目标对象不是 CGLIB 子类}当执行this.saveAndFail()时this指向的是原始类的实例而非 CGLIB 生成的代理子类。因此调用的是原始类中saveAndFail()的方法实现而非代理子类的重写版本——代理链完全被跳过。即使使用 JDK 动态代理接口代理结论相同this依然是目标对象代理拦截同样被绕过。四、可运行示例环境Java 17、Spring Boot 3.3.0、H2 内存数据库。pom.xml 依赖parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion3.3.0/version/parentdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-jpa/artifactId/dependencydependencygroupIdcom.h2database/groupIdartifactIdh2/artifactIdscoperuntime/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependencies实体类EntityTable(namedemo)publicclassDemo{IdGeneratedValue(strategyGenerationType.IDENTITY)privateLongid;privateStringname;publicDemo(){}publicDemo(Stringname){this.namename;}publicLonggetId(){returnid;}publicStringgetName(){returnname;}}RepositorypublicinterfaceDemoRepositoryextendsJpaRepositoryDemo,Long{}核心复现代码3 行voidprocess(){saveAndFail();}// ① 自调用this 指向目标对象而非代理TransactionalvoidsaveAndFail(){// ② 事务注解在自调用路径下被忽略repo.save(newDemo(self-invocation));thrownewRuntimeException();// ③ 期望回滚实际数据已落库}完整 ServiceServicepublicclassDemoService{AutowiredprivateDemoRepositoryrepo;publicvoidprocess(){saveAndFail();// 自调用不经过代理}TransactionalpublicvoidsaveAndFail(){repo.save(newDemo(self-invocation));thrownewRuntimeException(intentional failure for test);}}测试类SpringBootTestclassSelfInvocationTest{AutowiredprivateDemoServicedemoService;AutowiredprivateDemoRepositorydemoRepository;TestvoidselfInvocation_shouldNOT_rollback(){assertThrows(RuntimeException.class,()-demoService.process());longcountdemoRepository.count();System.out.println(【自调用】异常后数据条数 count);assertThat(count).as(自调用场景下 Transactional 失效数据未回滚).isEqualTo(1);}}五、验证结果与边界预期输出【自调用】异常后数据条数 1——异常抛出后数据仍在数据库中事务未生效。实际输出自调用绕过代理TransactionInterceptor未被触发没有事务被创建。repo.save()以 auto-commit 落库throw仅传播异常不触发任何回滚逻辑count 1数据未被回滚。对照组如果通过代理对象调用saveAndFail()如 Controller 注入 Service 后直接调用事务正常生效异常抛出后count 0。常见失败及其原因与修复如果修改代码后数据仍然未回滚检查是否将saveAndFail()改为private方法——即使提取了独立 BeanCGLIB 代理也不会拦截private方法。修复方法是将该方法改为public。外层有Transactional的子场景即使外层方法有Transactional内层方法的REQUIRES_NEW等传播行为同样被忽略——方法运行在外层事务中隔离语义失效。这是排查时最容易误判的情况。思考在微服务架构中事务边界跨服务时提取独立 Bean 是否足以应对分布式事务的一致性需求如果团队大量使用自调用是应在架构层面强制约束 Bean 粒度还是接受 AspectJ 织入带来的构建复杂度参考资料Spring Framework Reference 6.x — Declarative Transaction Management with AnnotationsSpring Boot Reference 3.x — Data Access Transaction