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,产假中持续输出。点个赞,收藏防丢❤️

相关推荐
Ares-Wang2 小时前
flask》》多线程并发数据安全问题 threading.local werkzeug.local.Local
后端·python·flask
覆东流2 小时前
第2天:Python变量与数据类型
开发语言·后端·python
Rust研习社3 小时前
Rust Default 特征详解:轻松实现类型默认值
开发语言·后端·rust
南囝coding3 小时前
零成本打造专业域名邮箱:Cloudflare + Gmail 终极配置保姆级全攻略
前端·后端
李二毛4 小时前
看到 done=true,就说明前面的写入都可见吗?
后端
Master_Azur4 小时前
JavaEE之Stream流
后端
暮年4 小时前
List并发实现-Vector
后端
Rust研习社4 小时前
Rust Copy 特征详解|新手必看!再也不与 Clone 混淆
后端·rust·编程语言
Cache技术分享4 小时前
385. Java IO API - Chmod 示例:模拟 chmod 命令的文件权限更改
前端·后端