使用 @Transactional

除了基于 XML 的声明式事务配置方法外,您还可以使用基于注解的方法。直接在 Java 源代码中声明事务语义,使声明更接近受影响的代码。由于旨在以事务方式使用的代码几乎总是以这种方式部署,因此不存在过度耦合的危险。

标准的 jakarta.transaction.Transactional 注解也支持作为 Infra 自身注解的替代品。有关更多详细信息,请参阅 JTA 文档。

使用 @Transactional 注解所带来的易用性最好通过一个示例来说明,该示例将在后面的文本中解释。 考虑以下类定义:

  • Java

// 我们想要使其具有事务性的服务类
@Transactional
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) {
    // ...
  }
}

如上所述在类级别使用时,该注解表示声明类(及其子类)的所有方法的默认值。或者,可以单独注解每个方法。有关 Infra 认为哪些方法具有事务性的更多详细信息,请参阅 方法可见性。请注意,类级别的注解不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明,以便参与子类级别的注解。

当像上面这样的 POJO 类在 Infra 上下文中定义为 bean 时,您可以通过 @Configuration 类中的 @EnableTransactionManagement 注解使 bean 实例具有事务性。有关完整详细信息,请参阅 javadoc

在 XML 配置中,<tx:annotation-driven/> 标签提供了类似的便利:

<!-- 来自文件 '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"/>

  <!-- 启用基于注解的事务行为配置 -->
  <!-- 仍然需要一个 TransactionManager -->
  <tx:annotation-driven transaction-manager="txManager"/> (1)

  <bean id="txManager" class="infra.jdbc.datasource.DataSourceTransactionManager">
    <!-- (此依赖项在其他地方定义) -->
    <property name="dataSource" ref="dataSource"/>
  </bean>

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

</beans>
1 使 bean 实例具有事务性的行。
如果您要注入的 TransactionManager 的 bean 名称为 transactionManager,则可以省略 <tx:annotation-driven/> 标签中的 transaction-manager 属性。如果您要进行依赖注入的 TransactionManager bean 具有任何其他名称,则必须使用 transaction-manager 属性,如前面的示例所示。

反应式事务方法使用反应式返回类型,这与命令式编程安排相反,如下面的清单所示:

  • Java

// 我们想要使其具有事务性的反应式服务类
@Transactional
public class DefaultFooService implements FooService {

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

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

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

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

请注意,对于返回的 Publisher,在 Reactive Streams 取消信号方面有一些特殊的注意事项。有关更多详细信息,请参阅“使用 TransactionalOperator”下的 取消信号 部分。

方法可见性和代理模式下的 @Transactional

@Transactional 注解通常用于具有 public 可见性的方法。从 6.0 开始,默认情况下,对于基于类的代理,protected 或包可见的方法也可以设为事务性。请注意,基于接口的代理中的事务方法必须始终是 public 的,并且在被代理的接口中定义。对于这两种代理,只有通过代理传入的外部方法调用才会被拦截。

如果您希望在不同类型的代理之间对方法可见性进行一致的处理(直到 5.3 版本这都是默认设置),请考虑指定 publicMethodsOnly

/**
 * 注册一个自定义 AnnotationTransactionAttributeSource,并将
 * publicMethodsOnly 标志设置为 true,以一致地忽略非公共方法。
 * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
 */
@Bean
TransactionAttributeSource transactionAttributeSource() {
  return new AnnotationTransactionAttributeSource(true);
}

Infra TestContext Framework 默认也支持非私有的 @Transactional 测试方法。有关示例,请参阅测试章节中的 事务管理

您可以将 @Transactional 注解应用于接口定义、接口上的方法、类定义或类上的方法。但是,仅存在 @Transactional 注解不足以激活事务行为。@Transactional 注解仅仅是元数据,可以由相应的运行时基础设施使用,该基础设施使用该元数据来配置具有事务行为的适当 bean。在前面的示例中,<tx:annotation-driven/> 元素在运行时开启实际的事务管理。

Infra 团队建议您使用 @Transactional 注解具体类的方法,而不是依赖于接口中的注解方法,即使后者在 5.0 版本中对基于接口和目标类的代理均有效。由于 Java 注解不会从接口继承,因此在使用 AspectJ 模式时,编织基础设施仍然无法识别接口声明的注解,因此切面不会被应用。结果,您的事务注解可能会被静默忽略:您的代码可能看起来“正常工作”,直到您测试回滚场景。
在代理模式(默认模式)下,只有通过代理传入的外部方法调用会被拦截。这意味着自调用(实际上是目标对象中的方法调用目标对象的另一个方法)在运行时不会导致实际的事务,即使被调用的方法标记为 @Transactional 也是如此。此外,代理必须完全初始化才能提供预期的行为,因此您不应在初始化代码中依赖此功能——例如在 @PostConstruct 方法中。

如果您希望自调用也被事务包装,请考虑使用 AspectJ 模式(请参阅下表中的 mode 属性)。在这种情况下,首先就没有代理。相反,目标类被编织(即,其字节码被修改)以支持任何类型方法的 @Transactional 运行时行为。

Table 1. 注解驱动的事务设置
XML 属性 注解属性 默认值 描述

transaction-manager

N/A (see TransactionManagementConfigurer javadoc)

transactionManager

要使用的事务管理器的名称。仅当事务管理器的名称不是 transactionManager 时才需要,如前面的示例所示。

mode

mode

proxy

默认模式 (proxy) 使用 Infra AOP 框架处理要代理的注解 bean(遵循代理语义,如前所述,仅适用于通过代理传入的方法调用)。替代模式 (aspectj) 改为使用 Infra AspectJ 事务切面编织受影响的类,修改目标类字节码以应用于任何类型的方法调用。AspectJ 编织需要类路径中有 spring-aspects.jar,并且启用了加载时编织(或编译时编织)。(有关如何设置加载时编织的详细信息,请参阅 Infra 配置。)

proxy-target-class

proxyTargetClass

false

仅适用于 proxy 模式。控制为使用 @Transactional 注解的类创建什么类型的事务代理。如果 proxy-target-class 属性设置为 true,则创建基于类的代理。如果 proxy-target-classfalse 或省略该属性,则创建标准 JDK 基于接口的代理。(有关不同代理类型的详细检查,请参阅 代理机制。)

order

order

Ordered.LOWEST_PRECEDENCE

定义应用于使用 @Transactional 注解的 bean 的事务通知的顺序。(有关与 AOP 通知排序相关的规则的更多信息,请参阅 通知排序。)未指定顺序意味着 AOP 子系统确定通知的顺序。

处理 @Transactional 注解的默认通知模式是 proxy,它仅允许通过代理拦截调用。同一类中的本地调用无法以这种方式被拦截。对于更高级的拦截模式,请考虑切换到 aspectj 模式并结合编译时或加载时编织。
proxy-target-class 属性控制为使用 @Transactional 注解的类创建什么类型的事务代理。如果 proxy-target-class 设置为 true,则创建基于类的代理。如果 proxy-target-classfalse 或省略该属性,则创建标准 JDK 基于接口的代理。(有关不同代理类型的讨论,请参阅 代理机制。)
@EnableTransactionManagement<tx:annotation-driven/> 仅在定义它们的同一个应用程序上下文中查找 @Transactional bean。这意味着,如果您将注解驱动的配置放在 MockDispatcherHandlerWebApplicationContext 中,它只会检查控制器中的 @Transactional bean,而不会检查服务中的。有关更多信息,请参阅 MVC

在评估方法的事务设置时,最具体的定义位置优先。在以下示例中,DefaultFooService 类在类级别使用了只读事务的设置进行注解,但同一类中 updateFoo(Foo) 方法上的 @Transactional 注解优先于在类级别定义的事务设置。

  • Java

@Transactional(readOnly = true)
public class DefaultFooService implements FooService {

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

  // 这些设置对于此方法具有优先权
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public void updateFoo(Foo foo) {
    // ...
  }
}

@Transactional 设置

@Transactional 注解是指定接口、类或方法必须具有事务语义的元数据(例如,“当调用此方法时启动一个全新的只读事务,挂起任何现有事务”)。 默认的 @Transactional 设置如下:

  • 传播设置是 PROPAGATION_REQUIRED

  • 隔离级别是 ISOLATION_DEFAULT

  • 事务是读写的。

  • 事务超时默认为底层事务系统的默认超时,如果不支持超时,则默认为无。

  • 任何 RuntimeExceptionError 都会触发回滚,而任何受检 Exception 不会。

您可以更改这些默认设置。下表总结了 @Transactional 注解的各种属性:

Table 2. @Transactional 设置
属性 类型 描述

value

String

指定要使用的事务管理器的可选限定符。

transactionManager

String

value 的别名。

label

String 标签数组

用于向事务添加表达性描述的标签数组。事务管理器可以评估标签,以将特定于实现的行为与实际事务相关联。

propagation

enum: Propagation

可选的传播设置。

isolation

enum: Isolation

可选的隔离级别。仅适用于 REQUIREDREQUIRES_NEW 的传播值。

timeout

int (以秒为单位)

可选的事务超时。仅适用于 REQUIREDREQUIRES_NEW 的传播值。

timeoutString

String (以秒为单位)

String 值指定以秒为单位的 timeout 的替代方法——例如,作为占位符。

readOnly

boolean

读写与只读事务。仅适用于 REQUIREDREQUIRES_NEW 的值。

rollbackFor

Class 对象数组,必须派生自 Throwable

必须导致回滚的可选异常类型数组。

rollbackForClassName

异常名称模式数组。

必须导致回滚的可选异常名称模式数组。

noRollbackFor

Class 对象数组,必须派生自 Throwable

必须不导致回滚的可选异常类型数组。

noRollbackForClassName

异常名称模式数组。

必须不导致回滚的可选异常名称模式数组。

有关回滚规则语义、模式以及关于基于模式的回滚规则可能出现的意外匹配的警告的更多详细信息,请参阅 回滚规则

从 6.2 开始,您可以全局更改默认回滚行为——例如,通过 @EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS),导致事务内引发的所有异常(包括任何受检异常)都回滚。为了进一步自定义,AnnotationTransactionAttributeSource 为自定义默认规则提供了 addDefaultRollbackRule(RollbackRuleAttribute) 方法。

请注意,特定于事务的回滚规则会覆盖默认行为,但会保留未指定异常的所选默认值。Infra @Transactional 以及 JTA 的 jakarta.transaction.Transactional 注解都是这种情况。

除非您依赖具有提交行为的 EJB 风格的业务异常,否则建议切换到 ALL_EXCEPTIONS 以获得一致的回滚语义,即使在(可能是意外的)受检异常的情况下也是如此。此外,对于完全不强制执行受检异常的基于 Kotlin 的应用程序,建议进行此切换。

目前,您无法显式控制事务的名称,这里的“名称”是指出现在事务监视器和日志输出中的事务名称。对于声明式事务,事务名称始终是事务通知类的完全限定类名 + . + 方法名。例如,如果 BusinessService 类的 handlePayment(..) 方法启动了事务,则事务名称将为 com.example.BusinessService.handlePayment

多个事务管理器与 @Transactional

大多数 Infra 应用程序只需要一个事务管理器,但在某些情况下,您可能希望在单个应用程序中使用多个独立的事务管理器。您可以使用 @Transactional 注解的 valuetransactionManager 属性来选择性地指定要使用的 TransactionManager 的标识。这可以是事务管理器 bean 的 bean 名称或限定符值。例如,使用限定符表示法,您可以将以下 Java 代码与应用程序上下文中的以下事务管理器 bean 声明结合使用:

  • Java

public class TransactionalService {

  @Transactional("order")
  public void setSomething(String name) { ... }

  @Transactional("account")
  public void doSomething() { ... }

  @Transactional("reactive-account")
  public Mono<Void> doSomethingReactive() { ... }
}

以下清单显示了 bean 声明:

<tx:annotation-driven/>

  <bean id="transactionManager1" class="infra.jdbc.support.JdbcTransactionManager">
    ...
    <qualifier value="order"/>
  </bean>

  <bean id="transactionManager2" class="infra.jdbc.support.JdbcTransactionManager">
    ...
    <qualifier value="account"/>
  </bean>

  <bean id="transactionManager3" class="infra.data.r2dbc.connection.R2dbcTransactionManager">
    ...
    <qualifier value="reactive-account"/>
  </bean>

在这种情况下,TransactionalService 上的各个方法在单独的事务管理器下运行,通过 orderaccountreactive-account 限定符进行区分。如果没有找到具体限定的 TransactionManager bean,仍然使用默认的 <tx:annotation-driven> 目标 bean 名称 transactionManager

如果同一类上的所有事务方法共享相同的限定符,请考虑声明一个类型级别的 infra.beans.factory.annotation.Qualifier 注解。如果其值与特定事务管理器的限定符值(或 bean 名称)匹配,则该事务管理器将用于 @Transactional 本身没有特定限定符的事务定义。

此类类型级别的限定符可以在具体类上声明,也适用于来自基类的事务定义。这实际上覆盖了任何未限定基类方法的默认事务管理器选择。

最后但同样重要的一点是,这种类型级别的 bean 限定符可以用于多种目的,例如,值为 "order" 的限定符可用于自动装配目的(标识订单存储库)以及事务管理器选择,只要自动装配的目标 bean 以及关联的事务管理器定义声明相同的限定符值即可。此类限定符值仅需在一组类型匹配的 bean 中唯一,而不必充当 ID。

自定义组合注解

如果您发现自己在许多不同方法上重复使用具有相同属性的 @TransactionalInfra 元注解支持 允许您为特定用例定义自定义组合注解。例如,考虑以下注解定义:

  • Java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}

前面的注解允许我们将上一节中的示例编写如下:

  • Java

public class TransactionalService {

  @OrderTx
  public void setSomething(String name) {
    // ...
  }

  @AccountTx
  public void doSomething() {
    // ...
  }
}

在前面的示例中,我们使用了定义事务管理器限定符和事务标签的语法,但我们也可以包含传播行为、回滚规则、超时和其他功能。