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事务默认只在RuntimeException 和Error时回滚。你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的循环依赖问题:
- 一级缓存:成品bean
- 二级缓存:半成品bean(实例化但未注入属性)
- 三级缓存:工厂对象
但这不是万能药,以下情况照样炸:
循环依赖的致命场景
场景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,产假中持续输出。点个赞,收藏防丢❤️