Spring框架“惯性思维”坑——@Transactional失效场景、Bean注入循环依赖

9年Java开发,Spring用了9年,但这些坑我依然踩过不止一次。今天聊两个"你以为你懂,其实不懂"的Spring陷阱: @Transactional各种不生效 ,以及循环依赖"能启动就是没问题"的错觉


一、@Transactional失效的4个经典场景

场景1:加在private方法上

java

typescript 复制代码
@Service
public class UserService {
    
    @Transactional  // ❌ 完全不生效,没有任何提示
    private void updateUser(User user) {
        userDao.update(user);
    }
}

为什么失效?

Spring事务通过动态代理实现。代理类只能拦截public方法,private方法无法被代理访问,注解被直接忽略。

解决方案:

java

typescript 复制代码
@Transactional
public void updateUser(User user) {  // ✅ 必须是public
    userDao.update(user);
}

记住:@Transactional只能加在public方法上,这不是建议,是强制要求。


场景2:同一个类内自调用

java

typescript 复制代码
@Service
public class UserService {
    
    public void outerMethod() {
        // ❌ 自调用,事务不生效
        this.innerMethod();
    }
    
    @Transactional
    public void innerMethod() {
        // 数据库操作
    }
}

为什么失效?

调用走的是this.,直接调用原始对象的方法,绕过了Spring代理。代理没有机会开启事务。

解决方案(3选1):

java

typescript 复制代码
// 方案1:注入自己(推荐)
@Service
public class UserService {
    @Autowired
    private UserService self;
    
    public void outerMethod() {
        self.innerMethod();  // ✅ 走代理,事务生效
    }
    
    @Transactional
    public void innerMethod() { }
}

java

typescript 复制代码
// 方案2:把事务方法放到另一个Service
@Service
public class UserService {
    @Autowired
    private TransactionService transactionService;
    
    public void outerMethod() {
        transactionService.innerMethod();  // ✅ 跨类调用
    }
}

java

arduino 复制代码
// 方案3:通过ApplicationContext获取代理(不推荐,太重)

场景3:异常被try-catch吞掉

java

typescript 复制代码
@Transactional
public void updateOrder(Order order) {
    try {
        orderDao.update(order);
        // 可能抛出SQLException
    } catch (Exception e) {
        log.error("更新失败", e);
        // ❌ 异常被吞了,事务不会回滚
    }
}

为什么失效?

Spring事务默认只在RuntimeExceptionError时回滚。你catch了异常没往外抛,Spring以为一切正常,事务正常提交。

解决方案:

java

typescript 复制代码
// 方案1:不catch,让异常往外抛
@Transactional
public void updateOrder(Order order) {
    orderDao.update(order);  // 异常直接抛出
}

java

typescript 复制代码
// 方案2:必须catch时,手动回滚
@Transactional
public void updateOrder(Order order) {
    try {
        orderDao.update(order);
    } catch (Exception e) {
        log.error("更新失败", e);
        // ✅ 手动标记回滚
        TransactionAspectSupport.currentTransactionStatus()
            .setRollbackOnly();
    }
}

场景4:rollbackFor没指定checked异常

java

less 复制代码
// 默认配置
@Transactional  // 只回滚RuntimeException和Error

// 实际业务中可能抛SQLException(checked异常)
@Transactional
public void saveData() throws SQLException {
    // 如果抛出SQLException,事务不会回滚❌
}

解决方案:

java

java 复制代码
@Transactional(rollbackFor = Exception.class)  // ✅ 全部异常都回滚
public void saveData() throws SQLException {
    // 任何异常都会触发回滚
}

生产环境建议:统一用@Transactional(rollbackFor = Exception.class),别给自己挖坑。


二、Bean注入循环依赖:能启动不等于没风险

场景描述

java

less 复制代码
@Service
public class A {
    @Autowired
    private B b;  // A依赖B
}

@Service
public class B {
    @Autowired
    private A a;  // B依赖A
}

这个能启动吗?

  • 能启动 ,如果用的是字段注入(@Autowired)或Setter注入
  • 不能启动,如果用的是构造器注入

为什么能启动?------Spring的三级缓存

Spring通过三级缓存解决了单例bean的循环依赖问题:

  1. 一级缓存:成品bean
  2. 二级缓存:半成品bean(实例化但未注入属性)
  3. 三级缓存:工厂对象

但这不是万能药,以下情况照样炸:


循环依赖的致命场景

场景1:构造器注入循环依赖

java

kotlin 复制代码
@Service
public class A {
    private final B b;
    
    public A(B b) {  // ❌ 启动报错:循环依赖
        this.b = b;
    }
}

Spring无法解决构造器循环依赖,因为必须先有实例才能放进缓存。

解决方案: 改用字段注入或Setter注入,或者重新设计。


场景2:代理对象循环依赖(@Async、@Transactional)

java

less 复制代码
@Service
public class A {
    @Autowired
    private B b;
    
    @Transactional  // 产生代理对象
    public void methodA() { }
}

@Service
public class B {
    @Autowired
    private A a;  // 可能报错或产生意外行为
}

为什么有问题?

当bean被AOP代理(事务、异步、缓存等),Spring需要创建代理对象。代理对象循环依赖时,可能导致:

  • 启动失败
  • 代理失效
  • 事务不生效

场景3:prototype scope循环依赖

java

less 复制代码
@Component
@Scope("prototype")
public class A {
    @Autowired
    private B b;  // ❌ 原型scope无法解决循环依赖,直接报错
}

Spring根本不支持原型scope的循环依赖,因为原型bean不会被缓存。


循环依赖的正确解决姿势

方案 说明 推荐度
重构代码 提取共同逻辑到新Service,打破循环 ⭐⭐⭐⭐⭐
@Lazy延迟加载 注入时加@Lazy,用到时才初始化 ⭐⭐⭐⭐
Setter/字段注入 替代构造器注入 ⭐⭐⭐
ApplicationContext.getBean() 运行时获取,不推荐 ⭐⭐

代码示例:

java

less 复制代码
// 方案:@Lazy延迟加载
@Service
public class A {
    @Lazy
    @Autowired
    private B b;  // B只在第一次使用时才初始化
}

// 方案:重构,引入中间Service
@Service
public class CommonService {
    // 提取A和B的共同逻辑
}

@Service
public class A {
    @Autowired
    private CommonService commonService;  // A依赖CommonService
}

@Service
public class B {
    @Autowired
    private CommonService commonService;  // B也依赖CommonService
}
// 循环依赖被打破

三、总结速查表

陷阱 错误写法 正确姿势
事务private方法 @Transactional private void xxx() 必须是public
自调用 this.methodWithTx() 注入自己或放到其他Service
异常被吞 try-catch后不处理 抛异常或手动setRollbackOnly
checked异常不回滚 @Transactional默认 rollbackFor=Exception.class
构造器循环依赖 new A(B b) + new B(A a) 改字段注入或用@Lazy
代理对象循环依赖 事务+异步+循环依赖 拆解设计,减少AOP

四、互动一下

你因为@Transactional不生效,线上出过什么事故?

你遇到的最诡异的循环依赖是什么场景?

评论区见👇


下期预告: 避坑3------MyBatis的"明明写了SQL却不执行"(#{}和${}的区别、返回null的坑、分页插件失效)


我是小李,9年Java,产假中持续输出。点个赞,收藏防丢❤️

相关推荐
ConardLi6 分钟前
啊?我刚开源的 Skills 已经 7K Star 了?!
前端·人工智能·后端
道友可好24 分钟前
Git Worktree:一个仓库,多个分身
前端·后端·程序员
鱼鳞_33 分钟前
苍穹外卖-Day10(Spring task)
java·后端·spring
我是一颗柠檬38 分钟前
【Redis】事务与Lua脚本Day7(2026年)
数据库·redis·后端·lua·database
farerboy1 小时前
15-Java while 和 do...while循环
java·后端
i220818 Faiz Ul1 小时前
民谣网站|基于Springboot的民谣网站管理系统(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·民谣网站
Oneslide2 小时前
windows cmd输入输出都很卡
后端
ihuyigui2 小时前
国际企业办公短信接口
前端·后端·架构
雪隐2 小时前
AI股票小助手03-Tushare数据采集
人工智能·后端
foggyprojects2 小时前
Java 里动态 SQL 为什么总是越写越乱
后端