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

0 阅读5分钟

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


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

场景1:加在private方法上

java

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

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

解决方案:

java

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

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


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

java

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

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

解决方案(3选1):

java

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

java

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

java

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

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

java

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

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

解决方案:

java

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

java

// 方案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

// 默认配置
@Transactional  // 只回滚RuntimeException和Error

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

解决方案:

java

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

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


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

场景描述

java

@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

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

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

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


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

java

@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

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

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


循环依赖的正确解决姿势

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

代码示例:

java

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