声明式事务实现示例

考虑以下接口及其附带的实现。此示例使用 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);

}

以下示例显示了前面接口的一个实现:

  • Java

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

假设 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="infra.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)。 然后,您使用 advisor 将切入点与 txAdvice 关联。结果表明,在 fooServiceOperation 执行时, 将运行由 txAdvice 定义的通知。

<aop:pointcut/> 元素中定义的表达式是一个 AspectJ 切入点表达式。有关 Infra 中切入点表达式的更多详细信息, 请参阅 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

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

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

<!-- Infra 容器正在启动... -->
[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)

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

TODAY Framework 使用 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);

}

以下示例显示了前面接口的一个实现:

  • 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) {
    // ...
  }
}

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

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

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

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

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