宣告式交易實作範例

請考慮以下介面及其隨附的實作。此範例使用 FooBar 類別作為佔位符,以便您可以專注於交易的使用,而無需關注特定的網域模型。就本範例而言,DefaultFooService 類別在每個實作方法的本體中擲回 UnsupportedOperationException 實例的事實是好的。此行為可讓您看到交易被建立,然後因應 UnsupportedOperationException 實例而被回滾。以下列表顯示 FooService 介面

  • Java

  • Kotlin

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

以下範例顯示上述介面的實作

  • 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) 必須在具有讀寫語意的交易環境中執行。以下組態將在接下來的幾個段落中詳細說明

<!-- 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 開頭的方法都將在唯讀交易的環境中執行,而所有其他方法都將以預設交易語意執行」。<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) 中定義的任何操作的執行。然後,您使用 Advisor 將切入點與 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 定義建立的物件周圍建立交易式 Proxy。Proxy 使用交易建議進行組態,以便在 Proxy 上叫用適當的方法時,將根據與該方法關聯的交易組態來啟動、暫停、標記為唯讀等等交易。請考慮以下程式,該程式測試驅動先前顯示的組態

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

執行上述程式的輸出應類似於以下內容(為了清楚起見,已截斷 Log4J 輸出和 DefaultFooService 類別的 insertFoo(..) 方法擲回的 UnsupportedOperationException 的堆疊追蹤)

<!-- 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 Framework 使用 ReactiveAdapterRegistry 來判斷方法傳回類型是否為反應式。

以下列表顯示先前使用的 FooService 的修改版本,但這次程式碼使用反應式類型

  • Java

  • Kotlin

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

以下範例顯示上述介面的實作

  • 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 可以在交易進行中但不必完成的情況下發出資料。因此,依賴於整個交易成功完成的方法需要確保完成並在呼叫程式碼中緩衝結果。