当前位置:首页 > 文章列表 > 文章 > java教程 > Java Spring Transactional 自调用为什么不会开启事务

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

来源:17golang原创 2026-09-07 12:53:38 0浏览 收藏

很多“@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删除
Go HTTP 客户端怎么复用 Transport 并设置连接池上限Go HTTP 客户端怎么复用 Transport 并设置连接池上限
上一篇
Go HTTP 客户端怎么复用 Transport 并设置连接池上限
Go reflect.Value.CanSet 为 false 时该怎么定位来源
下一篇
Go reflect.Value.CanSet 为 false 时该怎么定位来源
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    172次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    102次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    24次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    35次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    74次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码