You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

添加@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 17:47:50