添加@EnableTransactionManagement后@Transactional不回滚RuntimeException问题
问题描述
我把Spring Boot应用配置成这样:
@SpringBootApplication @EnableTransactionManagement public class MyApp extends SpringBootServletInitializer { ... }
另外还有这个类:
public class DoStaff { public void doStaffOnAll(List<MyObject> myObjects) { for (int i=0; i<myObjects.size(); i++) { try { doStaffOnSingle(myObjects.get(i), i); } catch (Exception e) { e.printStackTrace(); } } } @Transactional public void doStaffOnSingle(MyObject myObject, int i) { repository.save(myObject); if (i%2==0) { throw new RuntimeException(); } } }
当调用DoStaff.doStaffOnAll方法传入MyObject列表时,代码会保存所有元素,且每隔一个元素会抛出RuntimeException。因为doStaffOnSingle方法标注了@Transactional,我预期每隔一个元素会被回滚,但实际所有元素都成功保存到数据库中。想问问这是为什么?我哪里操作错了?
问题原因与解决方案
这个问题我之前也踩过坑,核心原因是Spring的事务注解是靠动态代理实现的,内部方法调用根本触发不了事务逻辑。
具体来说:
- 当你给
doStaffOnSingle加@Transactional时,Spring会给DoStaff类生成一个代理对象。只有通过这个代理对象调用方法,Spring才会帮你开启、提交或者回滚事务。 - 但你在
doStaffOnAll里直接写doStaffOnSingle(...),这是同一个类内部的直接调用,完全没经过代理对象。所以这个@Transactional相当于白加了,不管抛不抛异常,save操作都会直接落到数据库里,根本不会回滚。
那怎么解决呢?给你几个实用的方案:
方案1:拆分方法到独立服务类(最推荐)
把带@Transactional的方法抽到另一个单独的服务类里,比如叫StaffHandler,然后在DoStaff里注入这个类,通过注入的实例调用方法。这样调用就会走代理,事务就能正常工作了。示例代码:
@Service public class StaffHandler { @Autowired private Repository repository; @Transactional public void doStaffOnSingle(MyObject myObject, int i) { repository.save(myObject); if (i%2==0) { throw new RuntimeException(); } } } @Service public class DoStaff { @Autowired private StaffHandler staffHandler; public void doStaffOnAll(List<MyObject> myObjects) { for (int i=0; i<myObjects.size(); i++) { try { staffHandler.doStaffOnSingle(myObjects.get(i), i); } catch (Exception e) { e.printStackTrace(); } } } }
方案2:通过代理对象调用自身方法(应急方案)
如果不想拆分类,可以让DoStaff自己注入自己的代理对象,然后用这个代理对象调用doStaffOnSingle。不过这种方式有点“取巧”,不是常规写法,尽量少用:
@Service public class DoStaff { @Autowired private DoStaff selfProxy; // 注入自身的代理对象 public void doStaffOnAll(List<MyObject> myObjects) { for (int i=0; i<myObjects.size(); i++) { try { selfProxy.doStaffOnSingle(myObjects.get(i), i); // 通过代理调用,触发事务 } catch (Exception e) { e.printStackTrace(); } } } @Transactional public void doStaffOnSingle(MyObject myObject, int i) { repository.save(myObject); if (i%2==0) { throw new RuntimeException(); } } }
或者用AopContext获取当前代理对象,不过需要先在启动类开启暴露代理:
@SpringBootApplication @EnableTransactionManagement @EnableAspectJAutoProxy(exposeProxy = true) // 开启暴露代理 public class MyApp extends SpringBootServletInitializer { ... } // 在DoStaff的doStaffOnAll里这样调用 ((DoStaff)AopContext.currentProxy()).doStaffOnSingle(myObjects.get(i), i);
方案3:手动管理事务(灵活方案)
如果想完全摆脱代理的限制,可以用TransactionTemplate手动控制事务,这种方式更灵活,能自己掌控事务的开启和回滚:
@Service public class DoStaff { @Autowired private TransactionTemplate transactionTemplate; @Autowired private Repository repository; public void doStaffOnAll(List<MyObject> myObjects) { for (int i=0; i<myObjects.size(); i++) { int finalIndex = i; try { transactionTemplate.execute(status -> { repository.save(myObjects.get(finalIndex)); if (finalIndex%2==0) { throw new RuntimeException(); // 抛出异常会触发回滚 } return null; }); } catch (Exception e) { e.printStackTrace(); } } } }
内容的提问来源于stack exchange,提问作者victorio
相关产品推荐
相关产品推荐

