🔔@Transactional 中 finally 块抛异常后,事务到底回不回滚?

📌 前言
在 Spring 开发中,@Transactional 是我们最常用的声明式事务注解。但当 try-catch-finally 与 @Transactional 结合使用时,很多开发者对事务的回滚行为产生了困惑——如果 finally 块中抛出了异常,事务会回滚吗?原来 try 块中的异常还能触发回滚吗?
本文将从原理到实验,彻底讲清这个问题。
🔄 一、@Transactional 回滚机制回顾
在深入 finally 之前,先明确 @Transactional 的回滚规则:
| 场景 | 默认行为 |
|---|---|
| 方法正常返回 | 提交 |
抛出 RuntimeException 或 Error |
回滚 |
| 抛出受检异常(checked exception) | 提交(不回滚) |
Spring 的事务代理(AOP Proxy)包裹在方法外层,它只关心方法最终抛出了什么异常(或没有异常),以此决定提交还是回滚。
核心源码在 TransactionAspectSupport#invokeWithinTransaction 中:
// 简化后的伪代码
try {
// 调用目标方法(即我们的业务方法)
Object result = invocation.proceed();
// 正常返回 → 提交事务
commitTransactionAfterReturning(txInfo);
return result;
} catch (Throwable ex) {
// 异常抛出 → 判断是否需要回滚
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
关键结论:Spring 代理只能看到方法最终向外抛出的那个异常。 方法内部 try-catch-finally 的流转过程,代理是无感知的。
⚠️ 二、finally 块抛异常的 Java 语义
在分析事务行为之前,必须先理解一个纯 Java 层面的规则:
如果 finally 块中抛出了异常,它会覆盖(替换)try/catch 块中原本要抛出的异常。原始异常将丢失。
public void demo() {
try {
throw new RuntimeException("原始异常"); // ① 抛出
} finally {
throw new IllegalStateException("finally异常"); // ② 覆盖①
}
// 方法最终抛出的是 IllegalStateException("finally异常")
// RuntimeException("原始异常") 被静默吞掉了!
}
这个 Java 语义是理解后续所有场景的基础。
🔍 三、六大场景全解析
场景 1:try 正常执行,finally 也正常
@Transactional
public void scenario1() {
try {
userMapper.insert(new User("张三")); // 插入数据
} finally {
log.info("清理资源"); // 正常执行,无异常
}
}
结果:事务提交。 方法正常返回,代理执行 commit。
场景 2:try 抛出 RuntimeException,finally 正常
@Transactional
public void scenario2() {
try {
userMapper.insert(new User("张三"));
throw new RuntimeException("业务异常"); // ① 抛出
} finally {
log.info("清理资源"); // finally 正常执行
}
}
结果:事务回滚。 finally 没有抛异常,try 的 RuntimeException 正常传播到代理层,触发回滚。
场景 3:try 正常,finally 抛出 RuntimeException
@Transactional
public void scenario3() {
try {
userMapper.insert(new User("张三")); // 正常插入
} finally {
throw new RuntimeException("finally爆炸了"); // ① 抛出
}
}
结果:事务回滚。 方法最终抛出 RuntimeException,代理判定需要回滚。张三的数据不会入库。
这是最容易被忽视的场景。try 里的业务逻辑明明成功了,但 finally 的异常导致整个事务回滚。
场景 4:try 抛 RuntimeException,finally 也抛 RuntimeException
@Transactional
public void scenario4() {
try {
userMapper.insert(new User("张三"));
throw new RuntimeException("业务异常"); // ① 原始异常
} finally {
throw new RuntimeException("finally异常"); // ② 覆盖①
}
}
结果:事务回滚。 但注意:
- 代理层捕获的是
"finally异常",不是"业务异常" - 原始的业务异常被丢失了,日志中看不到真正的错误原因
- 这在排查问题时极为致命
场景 5:try 抛 RuntimeException,finally 抛 checked exception
@Transactional
public void scenario5() throws IOException {
try {
userMapper.insert(new User("张三"));
throw new RuntimeException("业务异常"); // ① 原始异常(unchecked)
} finally {
throw new IOException("finally IO异常"); // ② 覆盖①(checked)
}
}
结果:事务提交!数据不会回滚!
这是最反直觉的场景:
- try 块明确抛出了
RuntimeException,按理应该回滚 - 但 finally 的
IOException(checked exception)覆盖了原始异常 - 代理层只看到
IOException,默认不回滚 checked exception - 结果:张三的数据被提交了,而且原始的 RuntimeException 丢失了
场景 6:try 抛异常,finally 中 catch 吞掉了 finally 自身的异常
@Transactional
public void scenario6() {
try {
userMapper.insert(new User("张三"));
throw new RuntimeException("业务异常");
} finally {
try {
riskyCleanup(); // 可能抛异常的清理操作
} catch (Exception e) {
log.error("finally清理失败", e); // 吞掉finally中的异常
}
}
}
结果:事务回滚。 finally 内部的异常被 catch 了,不影响原始异常的传播。try 的 RuntimeException 正常到达代理层,触发回滚。
这是推荐的写法。
📊 四、场景汇总表
| 场景 | try 块 | finally 块 | 方法最终抛出 | 事务结果 |
|---|---|---|---|---|
| 1 | 正常 | 正常 | 无 | 提交 |
| 2 | RuntimeException | 正常 | RuntimeException | 回滚 |
| 3 | 正常 | RuntimeException | RuntimeException | 回滚 |
| 4 | RuntimeException | RuntimeException | finally 的异常 | 回滚(原始异常丢失) |
| 5 | RuntimeException | CheckedException | CheckedException | 提交(最危险!) |
| 6 | RuntimeException | catch 吞掉异常 | RuntimeException | 回滚(推荐写法) |
🧪 五、验证实验
下面通过一个完整的 Spring Boot 单元测试来验证上述结论:
@SpringBootTest
@Slf4j
public class TransactionalFinallyTest {
@Autowired
private UserService userService;
@Autowired
private UserMapper userMapper;
/**
* 场景5验证:try抛RuntimeException,finally抛checked exception
* 预期:事务不回滚(最危险的场景)
*/
@Test
public void testTryRuntimeExceptionFinallyCheckedException() {
try {
userService.riskyMethod();
} catch (Exception e) {
log.info("捕获到异常类型: {}", e.getClass().getSimpleName());
log.info("异常信息: {}", e.getMessage());
}
// 验证数据是否入库
User user = userMapper.selectByName("张三");
// 数据存在!事务没有回滚!
assertNotNull(user, "场景5验证:数据应该存在,因为事务未回滚");
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void riskyMethod() throws IOException {
try {
userMapper.insert(new User("张三"));
throw new RuntimeException("业务异常");
} finally {
throw new IOException("finally中的IO异常");
}
}
}
运行结果:
捕获到异常类型: IOException
异常信息: finally中的IO异常
// 断言通过 —— 张三的数据确实被提交了!
🧩 六、原理图解
方法调用链路:
Caller
└─→ Spring AOP Proxy(事务拦截器)
│
│ try {
│ Object result = targetMethod.invoke(); ←── 调用真实方法
│ tx.commit(); ←── 正常就提交
│ } catch (Throwable ex) {
│ if (isRollbackException(ex)) ←── 判断异常类型
│ tx.rollback(); ←── unchecked → 回滚
│ else
│ tx.commit(); ←── checked → 提交
│ }
│
└─→ 目标方法内部
│
│ try {
│ // 业务代码 → 可能抛异常A
│ } finally {
│ // 清理代码 → 可能抛异常B
│ }
│
│ 如果A和B都抛了 → 方法向外抛B(A丢失)
│ 代理只看到B → 根据B的类型决定回滚/提交
✅ 七、最佳实践
1. finally 中永远不要裸抛异常
// 错误写法
@Transactional
public void bad() {
try {
doBusiness();
} finally {
cleanup(); // 如果这里抛异常,可能覆盖业务异常
}
}
// 正确写法
@Transactional
public void good() {
try {
doBusiness();
} finally {
try {
cleanup();
} catch (Exception e) {
log.error("清理操作失败", e); // 记录日志但不向外抛
}
}
}
2. 使用 try-with-resources 代替手动 finally
@Transactional
public void better() {
try (Connection conn = dataSource.getConnection()) {
// 业务操作
}
// try-with-resources 的 close 异常不会覆盖 try 中的异常
// 而是作为 suppressed exception 附加上去
}
3. 如果必须在 finally 抛异常,保留原始异常
@Transactional
public void preserveOriginal() {
Throwable primaryException = null;
try {
doBusiness();
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
try {
cleanup();
} catch (Exception e) {
if (primaryException != null) {
primaryException.addSuppressed(e); // 保留两个异常
} else {
throw e;
}
}
}
}
4. 明确指定 rollbackFor
// 防止 checked exception 导致不回滚
@Transactional(rollbackFor = Exception.class)
public void safe() throws Exception {
try {
doBusiness();
} finally {
cleanup(); // 即使抛 checked exception 也会回滚
}
}
这是最简单有效的防御手段。建议团队规范中强制要求所有 @Transactional 都加上 rollbackFor = Exception.class。
💡 八、总结
| 要点 | 说明 |
|---|---|
| Spring 代理的视角 | 只看方法最终抛出的异常类型,不感知内部 try-finally 流转 |
| finally 异常会覆盖 try 异常 | 这是 Java 语言层面的行为,与 Spring 无关 |
| 最危险的场景 | try 抛 RuntimeException + finally 抛 CheckedException = 不回滚 |
| 核心防御 | @Transactional(rollbackFor = Exception.class) |
| finally 块铁律 | 永远在 finally 内部 try-catch,不要让异常逃逸 |
记住一句话:@Transactional 的回滚与否,取决于方法最终向外抛出的异常类型,而不是方法内部曾经发生过什么异常。 finally 块如果抛出异常,就会改变"最终向外抛出的异常",从而可能改变事务的回滚行为。
作者注:本文基于 Spring Framework 5.x / Spring Boot 2.x+ 验证,Spring 6.x 行为一致。如有疑问欢迎讨论。

浙公网安备 33010602011771号