声明式事务实现示例

考虑以下接口及其相关实现。此示例使用FooBar类作为占位符,以便您可以专注于事务使用,而不必关注特定的领域模型。在此示例中,DefaultFooService类在每个实现方法的主体中抛出UnsupportedOperationException实例是有益的。这种行为让您可以看到事务被创建,然后在响应UnsupportedOperationException实例时被回滚。以下清单显示了FooService接口:

  • Java

  • Kotlin

// 我们要使其具有事务性的服务接口

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

}
// 我们要使其具有事务性的服务接口

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

  • Kotlin

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)必须在具有读写语义的事务上下文中运行。下面的配置将在接下来的几段中详细解释:

<!-- 来自文件'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定义的事务性建议在程序中的适当点运行。首先,您定义一个匹配FooService接口中任何操作执行的切入点(fooServiceOperation)。然后,您通过使用顾问将切入点与txAdvice关联。结果表明,在执行fooServiceOperation时,将运行由txAdvice定义的建议。

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

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

<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

  • Kotlin

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

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

<!-- Spring容器正在启动... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - 为bean 'fooService'创建隐式代理,具有0个通用拦截器和1个特定拦截器

<!-- DefaultFooService实际上是被代理的 -->
[JdkDynamicAopProxy] - 为[x.y.service.DefaultFooService]创建JDK动态代理

<!-- ...现在在代理上调用insertFoo(..)方法 -->
[TransactionInterceptor] - 为x.y.service.FooService.insertFoo获取事务

<!-- 这里启动事务通知... -->
[DataSourceTransactionManager] - 为名称为[x.y.service.FooService.insertFoo]的新事务创建事务
[DataSourceTransactionManager] - 为JDBC事务获取连接[org.apache.commons.dbcp.PoolableConnection@a53de4]

<!-- DefaultFooService的insertFoo(..)方法抛出异常... -->
[RuleBasedTransactionAttribute] - 应用规则以确定java.lang.UnsupportedOperationException是否应回滚事务
[TransactionInterceptor] - 由于可抛出的异常[java.lang.UnsupportedOperationException],调用x.y.service.FooService.insertFoo上的事务回滚

<!-- 事务被回滚(默认情况下,RuntimeException实例会导致回滚) -->
[DataSourceTransactionManager] - 在连接[org.apache.commons.dbcp.PoolableConnection@a53de4]上回滚JDBC事务
[DataSourceTransactionManager] - 事务后释放JDBC连接
[DataSourceUtils] - 将JDBC连接返回给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 Framework使用ReactiveAdapterRegistry来确定方法返回类型是否为响应式。

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

  • Java

  • Kotlin

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

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

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

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

  • Kotlin

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使用事务操作符装饰返回的响应式类型以开始和清理事务。因此,调用具有事务的响应式方法将实际的事务管理推迟到激活处理响应式类型的订阅类型。

响应式事务管理的另一个方面涉及数据逃逸,这是编程模型的自然结果。

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

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

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