Rolling Back a Declarative Transaction

Spring 框架默认情况下会在遇到未处理的异常时回滚事务,包括运行时异常和 Error。从 Spring 框架 5.2 开始,还支持 Vavr 的 Try 方法,当其返回 "失败" 时触发回滚。自 Spring 框架 6.1 起,对 CompletableFuture 值的支持也得到了专门处理,如果它们在原始方法返回时已异常完成,则会触发回滚。

可以通过定义回滚规则来配置哪些异常类型会标记事务进行回滚,这些规则可以基于异常类型或模式。当回滚规则使用异常模式定义时,基于模式的回滚规则可能会导致意外匹配,因此必须仔细考虑模式的具体程度。

本文还简要提到了以编程方式指示必需的回滚,但强烈建议尽可能使用声明式方法。

前一节概述了如何以声明式的方式在应用程序中特定事务设置(通常是服务层类)的基本知识。本节介绍如何在 XML 配置中以简单、声明式的方式控制事务的回滚。有关使用 @Transactional 注解从声明式方式控制回滚语义的详细信息,请参见@Transactional Settings

向 Spring 框架的事务基础架构指示事务的工作将回滚的推荐方法是从当前在事务上下文中执行的代码中抛出一个 Exception。Spring 框架的事务基础架构代码会捕获任何未处理的 Exception,因为它会在调用堆栈中冒泡并确定是否将事务标记为回滚。

在其默认配置中,Spring 框架的事务基础架构代码仅在运行时未检查异常的情况下才会将事务标记为回滚。也就是说,当抛出的异常是 RuntimeException 的实例或子类时。(Error 实例也默认会导致回滚。)

在 Spring Framework 5.2 中,默认配置还支持 Vavr 的 Try 方法,当返回“失败”时触发事务回滚。这允许你使用 Try 处理函数式样式的错误,并且在发生故障时自动回滚事务。有关 Vavr 的 Try 的更多信息,请参阅 official Vavr documentation。以下是如何将 Vavr 的 Try 与事务方法配合使用的示例:

  • Java

@Transactional
public Try<String> myTransactionalMethod() {
	// If myDataAccessOperation throws an exception, it will be caught by the
	// Try instance created with Try.of() and wrapped inside the Failure class
	// which can be checked using the isFailure() method on the Try instance.
	return Try.of(delegate::myDataAccessOperation);
}

从 Spring Framework 6.1 开始,对 CompletableFuture(和通用 Future)返回值也进行了专门处理,如果在原始方法返回时它已异常完成,则会触发对该句柄的回滚。这是针对 @Async 方法设计的,其中实际方法实现可能需要符合 CompletableFuture 签名(运行时通过 @Async 处理自动适配到调用的实际异步句柄),宁愿在返回的句柄中公开也不愿重新抛出异常:

  • Java

@Transactional @Async
public CompletableFuture<String> myTransactionalMethod() {
	try {
		return CompletableFuture.completedFuture(delegate.myDataAccessOperation());
	}
	catch (DataAccessException ex) {
		return CompletableFuture.failedFuture(ex);
	}
}

从事务方法抛出的已检查异常不会导致默认配置中的回滚。你可以配置哪些 Exception 类型标记事务进行回滚,包括通过指定 回滚规则 来检查异常。

Rollback rules

回滚规则确定当抛出给定异常时是否应回滚事务,并且这些规则基于异常类型或异常模式。 回滚规则可以通过 rollback-forno-rollback-for 属性在 XML 中配置,这些属性允许将规则定义为模式。使用 @Transactional 时,可以通过 rollbackFor/ noRollbackForrollbackForClassName/ noRollbackForClassName 属性配置回滚规则,这些属性允许分别根据异常类型或模式定义规则。 当回滚规则使用异常类型定义时,该类型将用于与抛出的异常类型及其超类型进行匹配,从而提供类型安全并避免在使用模式时可能发生的任何意外匹配。例如,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 Framework 的事务基础设施,即使遇到未处理的 InstrumentNotFoundException,也要提交随后的事务:

<tx:advice id="txAdvice">
	<tx:attributes>
		<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
		<tx:method name="*"/>
	</tx:attributes>
</tx:advice>

当 Spring Framework 的事务基础设施捕获异常并查阅已配置的回滚规则以确定是否标记事务进行回滚时,最强的匹配规则获胜。因此,在以下配置的情况下,除了 InstrumentNotFoundException 之外的任何异常都会导致随后事务的回滚:

<tx:advice id="txAdvice">
	<tx:attributes>
		<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
	</tx:attributes>
</tx:advice>

你还可以以编程方式指示必需的回滚。虽然很简单,但这个过程相当侵入,并严格地将你的代码与 Spring Framework 的事务基础设施耦合在一起。以下示例展示如何以编程方式指示必需的回滚:

  • Java

  • Kotlin

public void resolvePosition() {
	try {
		// some business logic...
	} catch (NoProductInStockException ex) {
		// trigger rollback programmatically
		TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
	}
}
fun resolvePosition() {
	try {
		// some business logic...
	} catch (ex: NoProductInStockException) {
		// trigger rollback programmatically
		TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
	}
}

强烈建议你尽可能使用声明式方法进行回滚。如果你绝对需要,则可以进行编程回滚,但它的用法与实现干净的基于 POJO 的架构背道而驰。