当前位置:首页 > 文章列表 > 文章 > java教程 > Spring 自调用为什么不触发事务代理

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

来源:17golang原创 2026-11-12 01:02:04 0浏览 收藏

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删除
Java FileSystem.getPathMatcher 怎样区分 glob 与 regexJava FileSystem.getPathMatcher 怎样区分 glob 与 regex
上一篇
Java FileSystem.getPathMatcher 怎样区分 glob 与 regex
下一篇
下一篇
暂无
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    986次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    991次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1113次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1083次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    928次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码