Skip to content

声明式事务实现示例 (Example of Declarative Transaction Implementation)

考虑以下接口及其附带的实现。本例使用 FooBar 类作为占位符,以便你可以专注于事务的使用,而无需关注特定的领域模型。为了本例的目的,DefaultFooService 类在每个实现方法的主体中抛出 UnsupportedOperationException 实例是恰当的。这种行为让你可以看到事务被创建,然后响应 UnsupportedOperationException 实例而回滚。

以下清单显示了 FooService 接口:

java
// 我们希望使其具有事务性的服务接口

package x.y.service;

public interface FooService {

	Foo getFoo(String fooName);

	Foo getFoo(String fooName, String barName);

	void insertFoo(Foo foo);

	void updateFoo(Foo foo);

}
kotlin
// 我们希望使其具有事务性的服务接口

package x.y.service

interface FooService {

	fun getFoo(fooName: String): Foo

	fun getFoo(fooName: String, barName: String): Foo

	fun insertFoo(foo: Foo)

	fun updateFoo(foo: Foo)
}

以下示例显示了上述接口的实现:

java
package x.y.service;

public class DefaultFooService implements FooService {

	@Override
	public Foo getFoo(String fooName) {
		// ...
		throw new UnsupportedOperationException();
	}

	@Override
	public Foo getFoo(String fooName, String barName) {
		// ...
		throw new UnsupportedOperationException();
	}

	@Override
	public void insertFoo(Foo foo) {
		// ...
		throw new UnsupportedOperationException();
	}

	@Override
	public void updateFoo(Foo foo) {
		// ...
		throw new UnsupportedOperationException();
	}
}
kotlin
package x.y.service

class DefaultFooService : FooService {

	override fun getFoo(fooName: String): Foo {
		// ...
		throw UnsupportedOperationException()
	}

	override fun getFoo(fooName: String, barName: String): Foo {
		// ...
		throw UnsupportedOperationException()
	}

	override fun insertFoo(foo: Foo) {
		// ...
		throw UnsupportedOperationException()
	}

	override fun updateFoo(foo: Foo) {
		// ...
		throw UnsupportedOperationException()
	}
}

假设 FooService 接口的前两个方法 getFoo(String)getFoo(String, String) 必须在具有只读语义的事务上下文中运行,而其他方法 insertFoo(Foo)updateFoo(Foo) 必须在具有读写语义的事务上下文中运行。接下来的几段详细解释了以下配置:

xml
<!-- 来自文件 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/tx
		https://www.springframework.org/schema/tx/spring-tx.xsd
		http://www.springframework.org/schema/aop
		https://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- 这是我们想要使其具有事务性的服务对象 -->
	<bean id="fooService" class="x.y.service.DefaultFooService"/>

	<!-- 事务通知(即发生了“什么”;参见下文的 <aop:advisor/> bean) -->
	<tx:advice id="txAdvice" transaction-manager="txManager">
		<!-- 事务语义... -->
		<tx:attributes>
			<!-- 所有以 'get' 开头的方法都是只读的 -->
			<tx:method name="get*" read-only="true"/>
			<!-- 其他方法使用默认的事务设置(见下文) -->
			<tx:method name="*"/>
		</tx:attributes>
	</tx:advice>

	<!-- 确保上述事务通知在 FooService 接口定义的任何操作执行时运行 -->
	<aop:config>
		<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
	</aop:config>

	<!-- 别忘了 DataSource -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
		<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
		<property name="username" value="scott"/>
		<property name="password" value="tiger"/>
	</bean>

	<!-- 同样,别忘了 TransactionManager -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<!-- 此处为其他 <bean/> 定义 -->

</beans>

检查上述配置。它假设你想让服务对象 fooService Bean 具有事务性。要应用的事务语义封装在 <tx:advice/> 定义中。<tx:advice/> 定义的大意是:“所有以 get 开头的方法都在只读事务上下文中运行,所有其他方法都以默认事务语义运行”。<tx:advice/> 标签的 transaction-manager 属性被设置为驱动事务的 TransactionManager Bean 的名称(在本例中为 txManager Bean)。

提示

如果你想注入的 TransactionManager 的 Bean 名称是 transactionManager,那么你可以省略事务通知 (<tx:advice/>) 中的 transaction-manager 属性。如果你想注入的 TransactionManager Bean 有其他的名称,你必须像前面的例子那样显式使用 transaction-manager 属性。

<aop:config/> 定义确保由 txAdvice Bean 定义的事务通知在程序的适当点运行。首先,你定义一个切入点(pointcut),匹配 FooService 接口中定义的任何操作的执行 (fooServiceOperation)。然后,使用 advisor 将切入点与 txAdvice 关联起来。结果表明,在 fooServiceOperation 执行时,由 txAdvice 定义的通知将被运行。

<aop:pointcut/> 元素内定义的表达式是 AspectJ 切入点表达式。有关 Spring 中切入点表达式的更多详细信息,请参阅 AOP 章节

一个常见的需求是让整个服务层都具有事务性。做到这一点的最好方法是更改切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作:

xml
<aop:config>
	<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
	<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>

注意

在前面的示例中,假设你所有的服务接口都定义在 x.y.service 包中。有关更多详细信息,请参阅 AOP 章节

现在我们已经分析了配置,你可能会问自己,“所有这些配置实际上做了什么?”

前面显示的配置用于围绕从 fooService Bean 定义创建的对象创建一个事务代理。该代理配置了事务通知,以便当在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、挂起、标记为只读等事务操作。考虑以下测试驱动前面所示配置的程序:

java
public final class Boot {

	public static void main(final String[] args) throws Exception {
		ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
		FooService fooService = ctx.getBean(FooService.class);
		fooService.insertFoo(new Foo());
	}
}
kotlin
import org.springframework.beans.factory.getBean

fun main() {
	val ctx = ClassPathXmlApplicationContext("context.xml")
	val fooService = ctx.getBean<FooService>("fooService")
	fooService.insertFoo(Foo())
}

运行上述程序的输出应该类似于以下内容(为了清晰起见,Log4J 输出和由 DefaultFooService 类的 insertFoo(..) 方法抛出的 UnsupportedOperationException 的堆栈跟踪已被截断):

xml
<!-- Spring 容器正在启动... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- DefaultFooService 实际上已被代理 -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... 现在正在代理上调用 insertFoo(..) 方法 -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- 事务通知在此处生效... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- DefaultFooService 中的 insertFoo(..) 方法抛出异常... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]

<!-- 事务被回滚(默认情况下,RuntimeException 实例导致回滚) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- 为清晰起见,移除了 AOP 基础设施堆栈跟踪元素 -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

要使用响应式事务管理,代码必须使用响应式类型。

注意

Spring 框架使用 ReactiveAdapterRegistry 来确定方法返回类型是否是响应式的。

以下清单显示了前面使用的 FooService 的修改版本,但这次代码使用了响应式类型:

java
// 我们希望使其具有事务性的响应式服务接口

package x.y.service;

public interface FooService {

	Flux<Foo> getFoo(String fooName);

	Publisher<Foo> getFoo(String fooName, String barName);

	Mono<Void> insertFoo(Foo foo);

	Mono<Void> updateFoo(Foo foo);

}
kotlin
// 我们希望使其具有事务性的响应式服务接口

package x.y.service

interface FooService {

	fun getFoo(fooName: String): Flow<Foo>

	fun getFoo(fooName: String, barName: String): Publisher<Foo>

	fun insertFoo(foo: Foo) : Mono<Void>

	fun updateFoo(foo: Foo) : Mono<Void>
}

以下示例显示了上述接口的实现:

java
package x.y.service;

public class DefaultFooService implements FooService {

	@Override
	public Flux<Foo> getFoo(String fooName) {
		// ...
	}

	@Override
	public Publisher<Foo> getFoo(String fooName, String barName) {
		// ...
	}

	@Override
	public Mono<Void> insertFoo(Foo foo) {
		// ...
	}

	@Override
	public Mono<Void> updateFoo(Foo foo) {
		// ...
	}
}
kotlin
package x.y.service

class DefaultFooService : FooService {

	override fun getFoo(fooName: String): Flow<Foo> {
		// ...
	}

	override fun getFoo(fooName: String, barName: String): Publisher<Foo> {
		// ...
	}

	override fun insertFoo(foo: Foo): Mono<Void> {
		// ...
	}

	override fun updateFoo(foo: Foo): Mono<Void> {
		// ...
	}
}

命令式和响应式事务管理在事务边界和事务属性定义上共享相同的语义。命令式和响应式事务的主要区别在于后者的延迟特性TransactionInterceptor(事务拦截器)使用事务操作符装饰返回的响应式类型,以开始和清理事务。因此,调用事务性响应式方法会将实际的事务管理推迟到激活响应式类型处理的订阅(subscription)阶段。

响应式事务管理的另一个方面与数据逃逸 (data escaping) 有关,这是编程模型的自然结果。

命令式事务的方法返回值是在方法成功终止后从事务方法返回的,因此部分计算的结果不会逃逸出方法闭包。

响应式事务方法返回一个响应式包装类型,它代表一个计算序列以及开始和完成计算的承诺(promise)。

Publisher 可以在事务进行中但尚未完成时发出数据。因此,依赖于整个事务成功完成的方法需要确保完成并在调用代码中缓冲结果。


补充教学

1. 为什么还要学 XML 配置?

虽然现在的 Spring Boot 项目大多使用注解(@Transactional),但 XML 示例(<tx:advice><aop:config>)能更清晰地揭示“声明式事务”的本质——业务逻辑与事务逻辑的分离。 在 XML 中,你可以很清楚地看到:

  • Business Bean: DefaultFooService 是纯净的 Java 对象。
  • Transaction Logic: <tx:advice> 定义了“什么方法加什么事务”。
  • Binding: <aop:config> 将两者通过“切入点”绑定在一起。

这种“切面”的思想在 @Transactional 注解中被简化了,但也使得业务代码和配置逻辑耦合在了一起。

2. 日志解读:看懂 Spring 的内心戏

日志是调试事务问题的最好帮手。我们来看看关键日志行:

  • Creating implicit proxy: 这说明 AOP 开始工作了,FooService 不再是原来的对象,而被替换成了由 Spring 生成的代理对象(Proxy)。
  • Getting transaction for...: 代理对象拦截了方法调用,TransactionInterceptor 开始向事务管理器索要事务。
  • Creating new transaction: 事务管理器发现当前没有事务,于是新建一个。如果是 JDBC,这里会从数据库连接池获取连接并设置 autoCommit(false)
  • Invoking rollback ... due to throwable: 业务方法抛出了异常,拦截器捕获到了,决定回滚。
  • Rolling back JDBC transaction: 真正的数据库回滚操作。

3. 响应式事务的“延迟执行”与“数据逃逸”

响应式编程(Reactive Programming)的一个核心概念是惰性(Lazy)

  • 延迟特性:当你调用 fooService.updateFoo() 时,代码并没有真正执行数据库更新,它只是返回了一个“任务清单”(Mono/Flux)。只有当你(或框架,如 WebFlux 控制器)最终订阅(Subscribe)它时,事务才会开启,逻辑才会执行。
  • 数据逃逸:假设 getFoo() 返回一个流(Flux),事务在流开始时开启。如果流里有 10 个元素,Spring 会在流发射(Emit)数据的同时保持事务开启。
    • 风险:如果流发射了前 5 个数据给调用者(例如通过网络发给了 HTTP 客户端),然后第 6 个数据处理时报错了,事务虽然会回滚,但这前 5 个数据“已经发出去”了。这就是所谓的“数据逃逸”。
    • 解决:如果必须保证全有或全无,调用者通常需要 collectList() 先把所有数据缓冲下来,等待整个流完成(事务提交),再统一处理。但这会牺牲流式处理的内存和响应优势。

Based on Spring Framework.