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

Spring 自调用为什么不触发事务代理

来源:17golang原创

时间:2026-11-12 01:02:04 225浏览 收藏

Spring 自调用不触发事务代理,核心原因只有一个:默认的 proxy 模式拦截的是“经过代理对象进入 Bean”的调用,而同一个目标对象内部通过 this 调用方法时,调用链已经在目标对象内部,根本不会再次经过代理。于是,即使被调用的方法写了 @Transactional,事务增强也没有机会执行。

官方地址:https://spring.io/

处理时优先把事务方法拆到独立 Bean;如果暂时不能调整类结构,可以通过代理引用重新进入拦截链;只有明确需要拦截类内部调用时,才评估 AspectJ 织入。

先看清 Spring 事务代理拦截了什么

@Transactional 本身是事务元数据,实际行为由 Spring 的事务基础设施和 AOP 代理提供。外部组件拿到的是代理引用,调用入口大致是“调用方 → 代理 → 目标对象”。代理在把调用转给目标方法前,才有机会创建、加入或提交事务。

下面这张静态说明图把两条路径放在一起:蓝色路径从外部调用进入代理,红色路径从目标对象内部直接调用,后者不会回到代理。

Spring 代理与目标对象之间的外部调用和自调用边界说明图
图1:Spring 代理调用边界说明图;同类内部调用会绕过代理,不是运行截图。

为什么 this 调用会绕过 @Transactional

假设一个服务同时包含入口方法和事务方法:

@Service
public class OrderService {

    public void placeOrder() {
        // 这是目标对象内部调用,不会重新经过 Spring 代理
        this.saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // 这里的事务增强只有在调用经过代理时才会执行
        orderRepository.save(new Order());
    }
}

如果控制器或其他 Bean 调用的是代理上的 placeOrder(),placeOrder() 进入代理这一层可以被拦截;但方法体里的 this.saveOrder() 是 Java 对当前对象的直接调用。它没有一次新的代理入口,所以 saveOrder() 上的事务属性不会被代理读取。

把 this.saveOrder() 改成显式的 saveOrder() 也不会改变结果。省略 this 只是语法变化,调用仍然发生在同一个目标对象内部。

先排除几个容易混淆的前提

确认自调用问题前,先看调用对象和 Bean 生命周期,否则可能把多个问题混在一起:

  • 类是否由 Spring 容器创建。用 new OrderService() 得到的对象没有 Spring 代理。
  • 是否启用了事务管理。只有启用注解驱动事务,@Transactional 才会被运行时基础设施处理。
  • 调用方拿到的是 Bean 引用还是目标对象本身。只有经过代理的外部入口才有机会进入事务拦截链。
  • 接口代理下,事务方法是否是代理接口可见的公开方法;类代理和接口代理对可拦截方法的边界并不完全相同。
  • 是否在对象初始化阶段调用。代理尚未完整初始化时,不应把初始化代码当作正常事务入口。

这些前提都满足后,如果问题只发生在同类内部调用,基本就可以按“自调用绕过代理”处理。

最推荐:把事务方法拆到独立 Bean

拆分服务的思路最清楚:外层服务负责流程编排,内层服务负责事务边界。两个 Bean 之间的调用是外部调用,会经过内层 Bean 的代理。

@Service
public class OrderWriteService {

    @Transactional
    public void saveOrder() {
        // 事务边界放在独立 Bean 的公开入口上
        orderRepository.save(new Order());
    }
}

@Service
public class OrderService {

    private final OrderWriteService orderWriteService;

    public OrderService(OrderWriteService orderWriteService) {
        // 注入的是容器管理的 Bean 引用,调用时会经过代理
        this.orderWriteService = orderWriteService;
    }

    public void placeOrder() {
        // 跨 Bean 调用,事务代理可以拦截 saveOrder
        orderWriteService.saveOrder();
    }
}

这种方式的好处是依赖方向和事务职责都可见,也不会让业务类依赖 Spring AOP 的内部 API。对于长期维护的代码,通常应优先选择它。

不能拆分时:通过代理引用重新进入拦截链

如果类结构暂时不能调整,可以注入自己的代理引用,再通过这个引用调用事务方法。这里的关键不是“调用自己”,而是“调用代理对象代表的自己”。

@Service
public class OrderService {

    private final OrderService self;

    public OrderService(OrderService self) {
        // 容器注入的引用应当是代理,而不是手动 new 出来的目标对象
        this.self = self;
    }

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

    @Transactional
    public void saveOrder() {
        // 事务方法保持为公开入口,便于代理拦截
        orderRepository.save(new Order());
    }
}

自注入会增加类的间接性,也可能让循环依赖和代理类型变得更难理解,因此它更适合作为过渡方案。不要用 new 创建第二个实例来“模拟代理”,那样得到的仍然是没有容器增强的普通对象。

特殊需求:评估 AspectJ 织入

Spring 的 aspectj 模式不是通过代理拦截调用,而是把事务切面织入目标类的字节码,因此可以覆盖类内部调用。它需要相应的 AspectJ 依赖,以及编译期或加载期织入配置,部署和排障成本都高于默认代理模式。

@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class TransactionConfig {
    // 使用 AspectJ 模式,让事务切面织入目标类
    // 生产环境还需要按项目方式配置 spring-aspects 与织入过程
}

这里的选择不是“把 proxy 改成 aspectj 就万事大吉”。应先确认确实需要类内部调用也具备事务语义,再补齐织入依赖、构建配置和运行环境。若只是一个服务内部的流程调用,拆分 Bean 往往更容易理解。

拆分服务、代理引用和 AspectJ 织入三种 Spring 自调用处理方案对比图
图2:Spring 自调用处理方案对比结构图;用于理解关系,不代表本机运行结果。

用一张表选择处理方式

方式调用是否经过代理适合场景主要代价
拆分服务是,跨 Bean 调用新代码或允许调整职责需要重新划分类和依赖
代理引用是,通过代理再次进入暂时不能拆分的存量类间接性增加,依赖代理语义
AspectJ 织入不依赖代理入口必须覆盖内部调用的明确场景构建、部署和排障复杂度更高

排查时按这个顺序判断

  1. 确认调用对象来自 Spring 容器,而不是手动 new。
  2. 从调用点向前追踪:它是其他 Bean 调用,还是同类的 this 调用。
  3. 如果是同类调用,优先把事务方法移到独立 Bean。
  4. 如果必须保留原类,再检查注入的自引用确实指向代理。
  5. 只有确实需要内部调用也被增强时,才评估 AspectJ 的织入配置。

一句话总结:@Transactional 标注的是方法语义,但默认 proxy 模式只在调用穿过代理时应用这份语义;同类内部调用不穿过代理,所以不会因为方法上有注解就自动开启事务。

常见问题

把 this.method() 改成 method() 能解决吗?

不能。两者都是当前目标对象内部调用,仍然绕过 Spring 代理。

改成 CGLIB 代理后,自调用会生效吗?

通常不会。CGLIB 改变的是代理实现方式,不改变自调用绕过代理的调用路径。

可以直接使用 AopContext.currentProxy() 吗?

可以作为最后手段,但它会让业务类绑定 Spring AOP 上下文,并要求代理暴露当前代理。相比之下,拆分服务或注入代理引用更容易维护。

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