登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java Spring Transactional 自调用为什么不会开启事务

来源:17golang原创

时间:2026-09-07 12:53:38 129浏览 收藏

很多“@Transactional 没生效”的问题,根源不是注解写错,而是调用没有经过 Spring 代理。最典型的代码是:batchCreate() 在同一个类里调用 this.processOrder()。默认的 proxy 模式只拦截从代理对象进入的外部调用,类内的 this 调用直接落到目标对象,因此不会重新创建事务边界。

要点速览
  • Spring 默认用 AOP 代理处理事务,代理只看得到“从外部进来”的方法调用。
  • 自调用不等于没有任何事务:如果外层已有事务,内部方法可能加入外层事务;但它自己的传播、隔离或回滚入口不会被重新应用。
  • 优先拆分事务服务;确实不能拆分时再注入代理引用,AspectJ 织入适合有明确基础设施能力的项目。

为什么 this.processOrder() 不会走事务代理

可以把一个 Spring Bean 看成两层:容器暴露的是 OrderService代理,业务代码真正执行的是 OrderService目标对象。外部类调用代理时,事务拦截器有机会读取 @Transactional;进入目标对象后,this.batchCreate() 再调用 this.processOrder(),只是普通的 Java 虚方法调用。

下面这段代码的关键问题在调用路径,而不在注解位置:

@Service
public class OrderService {
    @Transactional
    public void batchCreate() {
        createOrder();
        // this 指向目标对象本身,不会再次经过 Spring 代理
        this.processOrder();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processOrder() {
        // 这里声明了新事务,但自调用时拦截器没有机会读取它
        markOrderProcessed();
    }
}

因此,如果 batchCreate() 是从代理进入的,processOrder() 可能仍在外层事务里执行;如果外层没有事务,它也不会因为自己的注解而单独开启事务。尤其是 REQUIRES_NEW、独立只读设置或单独的回滚规则,不能靠这次自调用自动生效。

Java Spring Transactional 自调用中 OrderController、代理、目标对象和事务拦截器的静态边界关系
图1:自调用停留在 OrderService 目标对象内部,没有再次穿过事务代理边界。

先用调用路径定位问题

排查时先问“谁持有这个对象引用”,不要先改传播级别。可以按下面的表快速判断:

现象优先检查说明
外部调用有事务,内部注解像没变化是否用了 this.method()内部调用绕过代理,不能重新应用方法级属性
从 Controller 调用也没有事务Bean 是否由 Spring 管理、事务管理是否启用对象若由 new 创建,容器无法为它建立代理
事务存在但没有回滚异常类型与回滚规则默认对 RuntimeException 和 Error 回滚,检查异常默认不回滚
public void logTransactionState(String scene) {
    // 只用于定位当前线程是否已经处于实际事务中
    boolean active = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("scene={}, transactionActive={}", scene, active);
}

这个状态只能证明“当前线程是否已有实际事务”,不能证明某个方法的 REQUIRES_NEW 是否生效。要确认代理边界,应该同时检查注入对象的类型,并把调用入口和异常路径记录下来。

三种修复方式怎么选

最稳妥的做法是把真正需要独立事务的方法拆到另一个 Bean,让调用自然跨过 Bean 边界:

@Service
public class OrderService {
    private final OrderTxService orderTxService;

    public OrderService(OrderTxService orderTxService) {
        this.orderTxService = orderTxService;
    }

    public void batchCreate() {
        createOrder();
        // 跨 Bean 调用,OrderTxService 的代理可以接管事务边界
        orderTxService.processOrder();
    }
}

@Service
public class OrderTxService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processOrder() {
        // 独立事务中的写操作
        markOrderProcessed();
    }
}

如果暂时无法拆分,可以注入当前 Bean 的代理引用,再通过代理调用;这种写法会引入自依赖和生命周期理解成本,使用 @Lazy 能避免部分初始化环问题:

@Service
public class OrderService {
    private final OrderService self;

    public OrderService(@Lazy OrderService self) {
        // Spring 注入的是代理引用,而不是 this
        this.self = self;
    }

    public void batchCreate() {
        // 通过代理进入,事务拦截器才有机会执行
        self.processOrder();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processOrder() {
        markOrderProcessed();
    }
}

第三种是 AspectJ 模式。它通过织入改变方法调用的拦截方式,可以覆盖自调用,但需要 spring-aspects、编译期或加载期织入等配套设施,排查成本也更高。不要为了修一处自调用就直接切换全局模式。

Java Spring 事务自调用的拆分服务、代理引用与 AspectJ 织入三种静态修复边界
图2:三种修复方式的关键差异,是事务方法是否位于可被代理或织入覆盖的边界上。

用回滚场景做反向验证

不要只看日志里出现了“调用成功”。准备一个可识别的写入,再主动抛出运行时异常,观察数据库状态:

  1. 从 Spring 注入的外部 Bean 调用入口,不要在测试里直接 new OrderService()
  2. processOrder() 写入一条带测试标记的记录,然后抛出 IllegalStateException
  3. 如果声明的是 REQUIRES_NEW,分别测试外层成功、外层失败和内层失败三种组合。
  4. 检查数据库记录、异常类型和事务活动日志,三者要能互相解释。

验证结果必须结合传播属性阅读:拆分 Bean 后,内层调用确实重新经过代理;但默认回滚规则仍然适用,检查异常是否回滚不能靠“已经走过代理”来推断。

常见问题

把 @Transactional 放到接口上就能解决自调用吗?

不能。接口注解与自调用是两个问题,调用仍然使用 this 时,依旧没有经过代理。事务方法更适合标在具体类的方法上,并先确认调用入口是容器管理的 Bean。

外层已经有事务时,自调用会完全失效吗?

不一定。内部代码可能继续运行在外层事务中,所以“数据库操作成功”不能证明内部注解生效;需要单独验证传播、隔离和回滚边界。

什么时候应该用 AspectJ?

当项目已经具备稳定的织入构建或运行基础设施,且确实需要覆盖类内调用时再考虑。普通业务服务通常优先拆分 Bean,边界更清楚,也更容易测试。

Spring 官方文档将默认事务模式定义为基于代理的拦截:只有经代理进入的调用才会触发事务建议,自调用不会自动产生新的事务边界。遇到类似问题,先画出“调用者—代理—目标对象”的关系,再选择修复方式,通常比继续调整注解参数更快。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>