回滚声明式事务 (Rolling Back a Declarative Transaction)
上一节介绍了如何在应用程序中以声明方式为类(通常是服务层类)指定事务设置的基础知识。本节描述如何通过 XML 配置以一种简单的声明式方式控制事务的回滚。有关使用 @Transactional 注解以声明方式控制回滚语义的详细信息,请参阅 @Transactional 设置。
向 Spring 框架的事务基础设施表明事务工作需要回滚的推荐方法是,从当前在事务上下文中执行的代码中抛出一个 Exception。Spring 框架的事务基础设施代码会捕获任何在调用堆栈中向上冒泡的未处理 Exception,并确定是否将事务标记为回滚。
在默认配置下,Spring 框架的事务基础设施代码仅在发生运行时、非检查型异常(unchecked exceptions)时才将事务标记为回滚。也就是说,当抛出的异常是 RuntimeException 的实例或子类时。(默认情况下,Error 实例也会导致回滚)。
默认配置还支持 Vavr 的 Try 方法,以便在返回 'Failure' 时触发事务回滚。这允许你使用 Try 来处理函数式风格的错误,并在发生故障时自动回滚事务。有关 Vavr Try 的更多信息,请参阅 Vavr 官方文档。 以下是如何在事务方法中使用 Vavr Try 的示例:
@Transactional
public Try<String> myTransactionalMethod() {
// 如果 myDataAccessOperation 抛出异常,它将被 Try.of() 创建的 Try 实例捕获
// 并包装在 Failure 类中,可以通过在 Try 实例上使用 isFailure() 方法进行检查。
return Try.of(delegate::myDataAccessOperation);
}从 Spring Framework 6.1 开始,对 CompletableFuture(以及一般的 Future)返回值也有特殊处理:如果返回的句柄在从原始方法返回时已异常完成(exceptionally completed),则触发回滚。这是为了配合 @Async 方法,因为其实际方法实现可能需要符合 CompletableFuture 签名(在运行时由 @Async 处理自动适配为调用代理的实际异步句柄),此时倾向于在返回的句柄中暴露异常而不是重新抛出异常:
@Transactional @Async
public CompletableFuture<String> myTransactionalMethod() {
try {
return CompletableFuture.completedFuture(delegate.myDataAccessOperation());
}
catch (DataAccessException ex) {
return CompletableFuture.failedFuture(ex);
}
}从事务方法抛出的检查型异常(Checked exceptions)在默认配置下不会导致回滚。你可以通过指定回滚规则 (rollback rules) 来准确配置哪些 Exception 类型将事务标记为回滚,包括检查型异常。
回滚规则 (Rollback rules)
回滚规则决定了当抛出给定异常时是否应回滚事务,这些规则基于异常类型或异常模式。
回滚规则可以通过 rollback-for 和 no-rollback-for 属性在 XML 中配置,这些属性允许将规则定义为模式。当使用 @Transactional 时,回滚规则可以通过 rollbackFor/noRollbackFor 和 rollbackForClassName/noRollbackForClassName 属性进行配置,它们分别允许基于异常类型或模式定义规则。
当使用异常类型定义回滚规则时——例如,通过 rollbackFor——该类型将用于与抛出的异常类型进行匹配。具体来说,给定配置的异常类型 C,如果抛出的异常类型 T 等于 C 或者是 C 的子类,则认为 T 与 C 匹配。这提供了类型安全性,并避免了使用模式时可能发生的任何意外匹配。例如,值 jakarta.servlet.ServletException.class 将仅匹配 jakarta.servlet.ServletException 类型及其子类的抛出异常。
当使用异常模式定义回滚规则时,模式可以是完全限定的类名或异常类型(必须是 Throwable 的子类)的完全限定类名的子字符串,目前不支持通配符。例如,值 "jakarta.servlet.ServletException" 或 "ServletException" 将匹配 jakarta.servlet.ServletException 及其子类。
警告
你必须仔细考虑模式的具体程度以及是否包含包信息(这不是强制性的)。例如,"Exception" 将匹配几乎任何内容,并可能会隐藏其他规则。如果 "Exception" 旨在为所有检查型异常定义规则,那么使用 "java.lang.Exception" 会更正确。对于更独特的异常名称,如 "BaseBusinessException",可能不需要使用该异常模式的完全限定类名。
此外,基于模式的回滚规则可能会导致名称相似的异常和嵌套类发生意外匹配。这是因为,如果抛出的异常名称包含为回滚规则配置的异常模式,则该抛出的异常被认为与给定的基于模式的回滚规则匹配。例如,给定一个配置为匹配 "com.example.CustomException" 的规则,该规则将匹配名为 com.example.CustomExceptionV2 的异常(与 CustomException 在同一个包中但带有额外后缀的异常)或名为 com.example.CustomException$AnotherException 的异常(在 CustomException 中声明为嵌套类的异常)。
以下 XML 片段演示了如何通过 rollback-for 属性提供异常模式,从而为检查型的、特定于应用程序的 Exception 类型配置回滚:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>如果你不希望在抛出异常时回滚事务,你也可以指定“不回滚”规则。以下示例告诉 Spring 框架的事务基础设施,即使面对未处理的 InstrumentNotFoundException,也要提交附带的事务:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>当 Spring 框架的事务基础设施捕获异常并查询配置的回滚规则以确定是否将事务标记为回滚时,最强匹配的规则获胜。因此,在以下配置的情况下,除了 InstrumentNotFoundException 之外的任何异常都会导致附带事务的回滚:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>你也可以通过编程方式指示必须回滚。虽然简单,但此过程具有很强的侵入性,并且将你的代码与 Spring 框架的事务基础设施紧密耦合。以下示例显示了如何以编程方式指示必须回滚:
public void resolvePosition() {
try {
// 一些业务逻辑...
} catch (NoProductInStockException ex) {
// 以编程方式触发回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}fun resolvePosition() {
try {
// 一些业务逻辑...
} catch (ex: NoProductInStockException) {
// 以编程方式触发回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}如果有任何可能,强烈建议你使用声明式方法进行回滚。如果你绝对需要,可以使用编程式回滚,但它的使用违背了实现纯 POJO 架构的初衷。
补充教学
1. 为什么默认只回滚 RuntimeException?
这是 Spring 框架设计中最常被问到的问题之一。这个设计来源于 EJB (Enterprise JavaBeans) 的惯例:
- RuntimeException / Error (Unchecked):通常代表系统级错误(System Exceptions),如数据库连接断开、空指针、算术溢出。这些错误通常是无法恢复的,因此默认回滚。
- Checked Exception:通常代表业务级异常(Application Exceptions),如“余额不足”、“用户不存在”。设计者认为,业务异常是业务流程的一部分,调用者应该捕获并处理它(例如给用户返回一个提示),而不一定意味着整个事务必须失败(或许可以尝试备用方案)。
最佳实践:在现代 Spring 开发中,建议自定义的业务异常也继承自 RuntimeException,这样可以让代码更简洁(免去繁琐的 throws),并且自动享受事务回滚。
2. 回滚规则的“最强匹配”原则
当你有多个规则时,Spring 会选择匹配度最高的那个。 例如配置:rollbackFor = Exception.class, noRollbackFor = MySpecialException.class。
- 如果抛出
MySpecialException:它既匹配 Exception (如父类),也匹配 MySpecialException (如自身)。但因为MySpecialException这个规则更具体(更接近抛出的异常类型),所以“不回滚”胜出。
3. 被忽视的 TransactionAspectSupport
虽然我们推崇声明式事务,但 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 在某些场景下是神器。 场景:你需要在一个大事务中做很多步操作,其中某一步出错了(catch住了异常),你希望记录日志并返回一个特定的错误码给前端,但同时你必须保证之前的数据库操作全部回滚。
try {
orderDao.save(order);
inventoryDao.decrease(item); // 抛出异常
} catch (Exception e) {
log.error("下单失败", e);
// 关键点:如果不加这行,Spring 认为你已经处理了异常,事务会正常提交!导致库存没扣但订单生成了。
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return "下单失败,请重试";
}如果不手动设置 RollbackOnly,被 catch 的异常不会被事务拦截器感知到,事务就会默认提交(Commit),从而导致严重的数据不一致。
4. 关于 Vavr 和 CompletableFuture 的新特性
Spring 6.1 引入的对 CompletableFuture 的异常回滚支持,主要是为了解决异步编排时的痛点。 以往,如果一个 @Transactional 方法返回 Future,事务拦截器在方法返回通过时(此时 Future 可能还没完成)就提交事务了。现在的支持允许在 Future 最终异常完成(CompletedExceptionally)时通过回调机制标记事务回滚,但这通常需要配合特定的异步执行模型(如 Kotlin Coroutines 或特定的代理机制)才能完美工作,单纯在同步的 PlatformTransactionManager 中使用 Future 需要非常小心线程上下文的问题。