使用 @Transactional (Using @Transactional)
除了基于 XML 的声明式事务配置外,你还可以使用基于注解的方法。直接在 Java 源代码中声明事务语义,可以使声明更贴近受影响的代码。基本上不存在过度耦合的风险,因为旨在以事务方式使用的代码几乎总是以这种方式部署的。
注意
标准的 jakarta.transaction.Transactional 注解也作为 Spring 自有注解的替代品受到支持。详情请参阅 JTA 文档。
通过一个例子最能说明使用 @Transactional 注解带来的易用性,该例子将在下文中解释。考虑以下类定义:
// 我们希望使其具有事务性的服务类
@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) {
// ...
}
}// 我们希望使其具有事务性的服务类
@Transactional
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) {
// ...
}
}如上所示,在类级别使用该注解表明该声明类(及其子类)的所有方法都具有默认的事务属性。或者,也可以单独标注每个方法。关于 Spring 认为哪些方法是事务性的,请参阅方法可见性以获取更多详细信息。请注意,类级别的注解不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明,以便参与到子类级别的注解中。
当像上面这样的 POJO 类在 Spring 上下文中定义为 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="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (此依赖项在别处定义) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 此处为其他 <bean/> 定义 -->
</beans>- 使 bean 实例具有事务性的行。
提示
如果你想注入的 TransactionManager 的 bean 名称是 transactionManager,你可以省略 <tx:annotation-driven/> 标签中的 transaction-manager 属性。如果你想注入的 TransactionManager bean 有其他的名称,你必须像前面的例子那样显式使用 transaction-manager 属性。
响应式事务方法使用响应式返回类型,这与命令式编程安排形成对比,如下面的清单所示:
// 我们希望使其具有事务性的响应式服务类
@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) {
// ...
}
}// 我们希望使其具有事务性的响应式服务类
@Transactional
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Flow<Foo> {
// ...
}
override fun getFoo(fooName: String, barName: String): Mono<Foo> {
// ...
}
override fun insertFoo(foo: Foo): Mono<Void> {
// ...
}
override fun updateFoo(foo: Foo): Mono<Void> {
// ...
}
}请注意,对于返回的 Publisher,在 Reactive Streams 取消信号方面有特殊的考虑。有关更多详细信息,请参阅“使用 TransactionalOperator”下的取消信号部分。
方法可见性和代理模式下的 @Transactional
@Transactional 注解通常用于具有 public 可见性的方法。从 6.0 开始,默认情况下,对于基于类的代理,protected 或包可见(package-visible)的方法也可以是事务性的。请注意,基于接口的代理中的事务方法必须始终是 public 的,并且在代理接口中定义。对于这两种类型的代理,只有通过代理传入的外部方法调用才会被拦截。
如果你更喜欢在不同类型的代理之间对方法可见性进行一致的处理(这也是直到 5.3 为止的默认设置),请考虑指定 publicMethodsOnly:
/**
* 注册一个将 publicMethodsOnly 标志设置为 true 的自定义 AnnotationTransactionAttributeSource,
* 以一致地忽略非公共方法。
* @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
*/
@Bean
TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(true);
}Spring TestContext Framework 默认情况下也支持非私有的 @Transactional 测试方法。有关示例,请参阅测试章节中的事务管理。
你可以将 @Transactional 注解应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在 @Transactional 注解并不足以激活事务行为。@Transactional 注解仅仅是元数据,可以被相应的运行时基础设施消费,这些基础设施使用该元数据来配置具有事务行为的适当 bean。在前面的例子中,<tx:annotation-driven/> 元素(或者 @EnableTransactionManagement)在运行时开启实际的事务管理。
提示
Spring 团队建议你在具体类的方法上使用 @Transactional 注解,而不是依赖接口上的注解方法,即使从 5.0 开始后者对于基于接口和基于目标类的代理都有效。由于 Java 注解不从接口继承,因此在使用 AspectJ 模式时,接口声明的注解仍然无法被织入基础设施识别,因此切面不会被应用。结果是,你的事务注解可能会被静默忽略:直到你测试回滚场景时,你的代码看起来可能都是“工作”的。
注意
在代理模式(这是默认模式)下,只有通过代理传入的外部方法调用才会被拦截。这意味着自调用(实际上是目标对象内的一个方法调用目标对象的另一个方法)在运行时不会导致实际的事务,即使被调用的方法标记了 @Transactional。此外,代理必须完全初始化才能提供预期的行为,因此你不应该在初始化代码中依赖此功能——例如,在 @PostConstruct 方法中。
如果你希望自调用也能被事务包裹,请考虑使用 AspectJ 模式(参见下表中的 mode 属性)。在这种情况下,首先就没有代理。相反,目标类被织入(即修改其字节码)以支持任何类型方法上的 @Transactional 运行时行为。
| XML 属性 | 注解属性 | 默认值 | 描述 |
|---|---|---|---|
transaction-manager | N/A (见 TransactionManagementConfigurer javadoc) | transactionManager | 要使用的事务管理器的名称。仅当事务管理器的名称不是 transactionManager 时才需要,如前面的示例所示。 |
mode | mode | proxy | 默认模式 (proxy) 处理带注解的 bean,使其由 Spring 的 AOP 框架代理(遵循代理语义,如前所述,仅适用于通过代理传入的方法调用)。另一种模式 (aspectj) 则使用 Spring 的 AspectJ 事务切面对受影响的类进行织入,修改目标类字节码以应用于任何类型的方法调用。AspectJ 织入需要 classpath 中有 spring-aspects.jar,并且启用了加载时织入(或编译时织入)。(有关如何设置加载时织入的详细信息,请参阅 Spring 配置。) |
proxy-target-class | proxyTargetClass | false | 仅适用于 proxy 模式。控制为使用 @Transactional 注解标注的类创建什么类型的事务代理。如果 proxy-target-class 属性设置为 true,则创建基于类的代理(CGLIB)。如果 proxy-target-class 为 false 或省略该属性,则创建标准的基于 JDK 接口的代理。(有关不同代理类型的详细检查,请参阅代理机制。) |
order | order | Ordered.LOWEST_PRECEDENCE | 定义应用于带有 @Transactional 注解的 bean 的事务通知的顺序。(有关 AOP 通知排序规则的更多信息,请参阅通知排序。)未指定顺序意味着 AOP 子系统决定通知的顺序。 |
注意
处理 @Transactional 注解的默认通知模式是 proxy,它只允许通过代理拦截调用。同一个类内的本地调用无法以这种方式被拦截。对于更高级的拦截模式,请考虑通过编译时或加载时织入切换到 aspectj 模式。
注意
proxy-target-class 属性控制为使用 @Transactional 注解标注的类创建什么类型的事务代理。如果 proxy-target-class 设置为 true,则创建基于类的代理。如果 proxy-target-class 为 false 或省略该属性,则创建标准的基于 JDK 接口的代理。(有关不同代理类型的讨论,请参阅代理机制。)
注意
@EnableTransactionManagement 和 <tx:annotation-driven/> 仅在其定义的同一应用上下文中查找 bean 上的 @Transactional。这意味着,如果你将注解驱动的配置放在 DispatcherServlet 的 WebApplicationContext 中,它只会在你的控制器中检查 @Transactional bean,而不会在你的服务中检查。有关更多信息,请参阅 MVC。
在评估方法的事务设置时,最具体的位置优先。在以下示例的情况下,DefaultFooService 类在类级别上使用了只读事务的设置进行注解,但同一类中 updateFoo(Foo) 方法上的 @Transactional 注解优先于类级别定义的事务设置。
@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(readOnly = true)
class DefaultFooService : FooService {
override fun getFoo(fooName: String): Foo {
// ...
}
// 这些设置对该方法优先
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
override fun updateFoo(foo: Foo) {
// ...
}
}@Transactional 设置
@Transactional 注解是元数据,指定接口、类或方法必须具有事务语义(例如,“调用此方法时启动一个全新的只读事务,挂起任何现有事务”)。 默认的 @Transactional 设置如下:
- 传播设置是
PROPAGATION_REQUIRED。 - 隔离级别是
ISOLATION_DEFAULT。 - 事务是读写的。
- 事务超时默认为底层事务系统的默认超时,或者如果不支持超时则没有超时。
- 任何
RuntimeException或Error都会触发回滚,而任何检查型Exception则不会。
你可以更改这些默认设置。下表总结了 @Transactional 注解的各种属性:
| 属性 | 类型 | 描述 |
|---|---|---|
value | String | 指定要使用的事务管理器的可选限定符。 |
transactionManager | String | value 的别名。 |
label | String 数组 | 添加到事务的描述性标签。事务管理器可以评估标签,以将特定于实现的行为与实际事务相关联。 |
propagation | enum: Propagation | 可选的传播设置。 |
isolation | enum: Isolation | 可选的隔离级别。仅适用于 REQUIRED 或 REQUIRES_NEW 的传播值。 |
timeout | int(秒粒度) | 可选的事务超时。仅适用于 REQUIRED 或 REQUIRES_NEW 的传播值。 |
timeoutString | String(秒粒度) | 指定超时的替代方法(秒),作为 String 值——例如,作为占位符。 |
readOnly | boolean | 读写事务与只读事务。仅适用于 REQUIRED 或 REQUIRES_NEW 的值。 |
rollbackFor | Class 对象数组(必须派生自 Throwable) | 必须导致回滚的异常类型的可选数组。 |
rollbackForClassName | 异常名称模式数组 | 必须导致回滚的异常名称模式的可选数组。 |
noRollbackFor | Class 对象数组(必须派生自 Throwable) | 必须不导致回滚的异常类型的可选数组。 |
noRollbackForClassName | 异常名称模式数组 | 必须不导致回滚的异常名称模式的可选数组。 |
提示
有关回滚规则语义、模式以及关于基于模式的回滚规则可能出现意外匹配的警告的更多详细信息,请参阅回滚规则。
注意
从 6.2 开始,你可以全局更改默认回滚行为——例如,通过 @EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS),导致事务内引发的所有异常(包括任何检查型异常)都回滚。对于进一步的自定义,AnnotationTransactionAttributeSource 提供了一个 addDefaultRollbackRule(RollbackRuleAttribute) 方法用于自定义默认规则。
请注意,特定于事务的回滚规则覆盖默认行为,但对于未指定的异常保留所选的默认值。这对于 Spring 的 @Transactional 以及 JTA 的 jakarta.transaction.Transactional 注解都是如此。
除非你依赖具有提交行为的 EJB 风格的业务异常,否则建议切换到 ALL_EXCEPTIONS 以获得一致的回滚语义,即使在(可能是意外的)检查型异常的情况下也是如此。此外,对于基于 Kotlin 的应用程序,建议进行此切换,因为 Kotlin 完全没有强制执行检查型异常。
目前,你无法显式控制事务的名称,这里的“名称”是指在事务监视器和日志输出中显示的事务名称。对于声明式事务,事务名称始终是经过事务通知的类的完全限定类名 + . + 方法名。例如,如果 BusinessService 类的 handlePayment(..) 方法启动了一个事务,事务的名称将是 com.example.BusinessService.handlePayment。
配合 @Transactional 使用多个事务管理器
大多数 Spring 应用程序只需要一个事务管理器,但在某些情况下,你可能希望在一个应用程序中使用多个独立的事务管理器。你可以使用 @Transactional 注解的 value 或 transactionManager 属性来可选地指定要使用的 TransactionManager。这可以是事务管理器 bean 的名称或限定符(qualifier)值。例如,使用限定符表示法,你可以结合以下 Java 代码和应用程序上下文中的事务管理器 bean 声明:
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}class TransactionalService {
@Transactional("order")
fun setSomething(name: String) {
// ...
}
@Transactional("account")
fun doSomething() {
// ...
}
@Transactional("reactive-account")
fun doSomethingReactive(): Mono<Void> {
// ...
}
}以下清单显示了 bean 声明:
<tx:annotation-driven/>
<bean id="transactionManager1" class="org.springframework.jdbc.support.JdbcTransactionManager">
...
<qualifier value="order"/>
</bean>
<bean id="transactionManager2" class="org.springframework.jdbc.support.JdbcTransactionManager">
...
<qualifier value="account"/>
</bean>
<bean id="transactionManager3" class="org.springframework.data.r2dbc.connection.R2dbcTransactionManager">
...
<qualifier value="reactive-account"/>
</bean>在这种情况下,TransactionalService 上的各个方法在单独的事务管理器下运行,通过 order、account 和 reactive-account 限定符进行区分。如果没有找到特定限定的 TransactionManager bean,仍然使用默认的 <tx:annotation-driven> 目标 bean 名称 transactionManager。
提示
如果同一类上的所有事务方法共享相同的限定符,请考虑声明一个类型级别的 org.springframework.beans.factory.annotation.Qualifier 注解。如果其值与特定事务管理器的限定符值(或 bean 名称)匹配,则该事务管理器将用于没有在 @Transactional 本身上指定限定符的事务定义。
这种类型级别的限定符可以在具体类上声明,也适用于基类中的事务定义。这有效地覆盖了任何未限定的基类方法的默认事务管理器选择。
最后但并非最不重要的一点是,这种类型级别的 bean 限定符可以有多种用途,例如,值为 "order" 的限定符既可以用于自动装配目的(识别 order 仓库),也可以用于事务管理器选择,只要自动装配的目标 bean 以及关联的事务管理器定义声明了相同的限定符值即可。这种限定符值只需要在一组类型匹配的 bean 中是唯一的,不需要作为 ID。
自定义组合注解
如果你发现你在许多不同的方法上重复使用具有相同属性的 @Transactional,Spring 的元注解支持允许你为特定用例定义自定义组合注解。例如,考虑以下注解定义:
@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 {
}@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "order", label = ["causal-consistency"])
annotation class OrderTx
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "account", label = ["retryable"])
annotation class AccountTx前面的注解让我们把上一节的例子写成如下形式:
public class TransactionalService {
@OrderTx
public void setSomething(String name) {
// ...
}
@AccountTx
public void doSomething() {
// ...
}
}class TransactionalService {
@OrderTx
fun setSomething(name: String) {
// ...
}
@AccountTx
fun doSomething() {
// ...
}
}在前面的例子中,我们使用语法定义了事务管理器限定符和事务标签,但我们也可以包含传播行为、回滚规则、超时和其他特性。
补充教学
1. @Transactional 无效的常见原因
虽然 @Transactional 很方便,但它是 Spring 论坛上求助率最高的话题之一。
- 方法不是 public 的:虽然从 Spring 6.0 开始放宽了限制(于类代理),但为了稳妥和兼容性,建议始终将事务方法设为
public。 - 同类自调用:在
this.methodA()中调用事务方法this.methodB()。因为绕过了代理,methodB的事务配置失效。 - 异常被吞了:如果在事务方法内部
try-catch了异常且没有抛出,事务拦截器捕获不到异常,自然不会回滚。 - Bean 没有被 Spring 管理:如果你
new Service()了一个对象来调用,那它就是个普通 Java 对象,没有任何事务功能。 - 跨容器扫描问题:这在传统的 Spring MVC 项目中常见。
DispatcherServlet只扫描 Controller,如果此时把 Service 也扫描了,可能导致产生两个 Bean,一个有事务一个没有(或配置被覆盖)。文档特别提到了这一点:“如果你把注解驱动配置放在 WebApplicationContext... 它不会在你的 Service 中检查”。
2. Spring 6.2 新特性:rollbackOn=ALL_EXCEPTIONS
这是一个巨大的变革! 几十年来,Spring 一直坚持“Unchecked 异常回滚,Checked 异常不回滚”的教条。但从 Spring 6.2 开始,你可以通过全局配置 @EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS) 来打破这个教条。 这对于 Kotlin 开发者以及厌倦了必须写 rollbackFor=Exception.class 的 Java 开发者来说是极大的福音。它让事务行为更加符合直觉:只要报错,就应该回滚。
3. 多事务管理器的最佳实践
当你有多个数据源时(如 MySQL + MongoDB),如何优雅地管理事务?
- 限定符 (Qualifier):不要直接用 Bean Name 引用,而是用
@Qualifier或@Transactional("qualifierName")。这样即使 Bean 重命名了,代码也不用改。 - 组合注解 (Composed Annotation):这是最优雅的方式。定义
@OrderDbTx和@AccountDbTx。- 好处 1:代码更语义化。
@OrderDbTx比@Transactional("tm1")清晰得多。 - 好处 2:修改方便。如果哪天 Order 数据库迁移了,只需要修改注解定义,无需修改成百上千个 Service 方法。
- 好处 1:代码更语义化。
4. 为什么 Spring 建议把注解加在具体类上而不是接口上?
这是一个关于 Java 动态代理 vs CGLIB 的历史遗留问题,也是 AspectJ 的限制。
- 接口注解:如果加在接口上,只有在使用 JDK 动态代理时才有效(因为 JDK 代理是基于接口的)。
- 类注解:如果加在类上,无论是 JDK 动态代理还是 CGLIB(基于继承类)都有效。
- AspectJ:完全无视接口上的注解。 为了避免“我换了一种代理模式,怎么事务失效了?”这种困惑,Spring 官方一刀切地建议:加在具体类上。
5. 方法可见性的变化 (Spring 6.0+)
文档提到 Spring 6.0 开始支持 protected/package-private 方法的事务(针对类代理)。这得益于 CGLIB 或 ByteBuddy 等字节码技术的进步。但这并不意味着你可以随意把事务加在 private 方法上。私有方法依然是代理的禁区(因为 private 方法不能被子类覆盖,而 CGLIB 恰恰是通过生成子类来工作的)。