对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

声明式事务实现示例

请考虑以下接口及其对应的实现。本示例使用 FooBar 类作为占位符,以便您能专注于事务的使用方式,而不必关注特定的领域模型。在本例中,DefaultFooService 类在每个已实现方法的主体中抛出 UnsupportedOperationException 实例,这种做法是合理的。该行为能让您观察到事务被创建后,又因 UnsupportedOperationException 实例而回滚的过程。以下代码清单展示了 FooService 接口:spring-doc.cadn.net.cn

// the service interface that we want to make transactional

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);

}
// the service interface that we want to make transactional

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)
}

以下示例展示了上述接口的实现:spring-doc.cadn.net.cn

package x.y.service;

public class DefaultFooService implements FooService {

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

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

	@Override
	public void insertFoo(Foo foo) {
		// ...
	}

	@Override
	public void updateFoo(Foo foo) {
		// ...
	}
}
package x.y.service

class DefaultFooService : FooService {

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

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

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

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

假设 FooService 接口的前两个方法 getFoo(String)getFoo(String, String) 必须在只读语义的事务上下文中运行,而其他方法 insertFoo(Foo)updateFoo(Foo) 则必须在读写语义的事务上下文中运行。以下配置将在接下来的几段中详细说明:spring-doc.cadn.net.cn

<!-- from the file '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">

	<!-- this is the service object that we want to make transactional -->
	<bean id="fooService" class="x.y.service.DefaultFooService"/>

	<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
	<tx:advice id="txAdvice" transaction-manager="txManager">
		<!-- the transactional semantics... -->
		<tx:attributes>
			<!-- all methods starting with 'get' are read-only -->
			<tx:method name="get*" read-only="true"/>
			<!-- other methods use the default transaction settings (see below) -->
			<tx:method name="*"/>
		</tx:attributes>
	</tx:advice>

	<!-- ensure that the above transactional advice runs for any execution
		of an operation defined by the FooService interface -->
	<aop:config>
		<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
	</aop:config>

	<!-- don't forget the 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>

	<!-- similarly, don't forget the TransactionManager -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<!-- other <bean/> definitions here -->

</beans>

请查看上述配置。它假定您希望将一个服务对象(即 fooService bean)设置为事务性的。<tx:advice/> 定义中封装了要应用的事务语义。<tx:advice/> 的定义含义是:“所有以 get 开头的方法都应在只读事务上下文中运行,而所有其他方法则使用默认的事务语义”。transaction-manager 标签的 <tx:advice/> 属性被设置为用于驱动事务的 TransactionManager bean 的名称(在本例中为 txManager bean)。spring-doc.cadn.net.cn

如果要注入的 transaction-manager bean 的名称为 <tx:advice/>,则可以在事务通知(TransactionManager)中省略 transactionManager 属性。如果要注入的 TransactionManager bean 使用了其他名称,则必须像前面示例中那样显式使用 transaction-manager 属性。

<aop:config/> 的定义确保了由 txAdvice bean 所定义的事务通知在程序中的适当位置执行。首先,您定义一个切入点(pointcut),该切入点匹配 FooService 接口中定义的任意操作的执行(fooServiceOperation)。然后,您通过一个通知器(advisor)将该切入点与 txAdvice 关联起来。其结果表明,在执行 fooServiceOperation 时,会运行由 txAdvice 定义的通知。spring-doc.cadn.net.cn

<aop:pointcut/> 元素中定义的表达式是一个 AspectJ 切点表达式。有关 Spring 中切点表达式的更多详细信息,请参阅AOP 章节spring-doc.cadn.net.cn

一个常见的需求是使整个服务层具有事务性。实现这一点的最佳方式是修改切入点表达式,使其匹配服务层中的任意操作。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

<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 章节

现在我们已经分析了配置,你可能在想, "所有这些配置究竟做了些什么?"spring-doc.cadn.net.cn

前面所示的配置用于围绕从 fooService bean 定义创建的对象生成一个事务代理。该代理配置了事务通知(advice),因此当在代理上调用适当的方法时,会根据与该方法关联的事务配置来启动、挂起、标记为只读等事务操作。请考虑以下程序,它用于测试前面所示的配置:spring-doc.cadn.net.cn

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());
	}
}
import org.springframework.beans.factory.getBean

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

运行 preceding 程序的输出应类似于以下内容(为了清晰,Log4J 输出和由 DefaultFooService 类的 insertFoo(..) 方法抛出的 UnsupportedOperationException 异常堆栈跟踪已被裁剪):spring-doc.cadn.net.cn

<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[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]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[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 infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

要使用响应式事务管理,代码必须使用响应式类型。spring-doc.cadn.net.cn

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

以下列表显示了之前使用的的修改版本,但这次代码使用了响应式类型:spring-doc.cadn.net.cn

// the reactive service interface that we want to make transactional

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);

}
// the reactive service interface that we want to make transactional

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>
}

以下示例展示了上述接口的实现:spring-doc.cadn.net.cn

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) {
		// ...
	}
}
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)阶段,此时才会激活对该响应式类型的处理。spring-doc.cadn.net.cn

另一方面的响应式事务管理涉及到数据逃逸,这是编程模型的自然结果。spring-doc.cadn.net.cn

方法实现的事务返回值会在事务性方法成功终止时从方法中返回,以确保部分计算的结果不逃逸出方法闭包。spring-doc.cadn.net.cn

响应式事务方法返回一个表示计算序列并承诺开始和完成该计算的响应式包装类型。spring-doc.cadn.net.cn

Publisher 可以在事务进行中(但不一定已完成)时发出数据。 因此,依赖于整个事务成功完成的方法需要在调用代码中确保事务完成并缓冲结果。spring-doc.cadn.net.cn