考虑一下下面的接口及其附带的实现。这个例子使用 Foo 和 Bar 类作为占位符,这样你就可以专注于事务的使用,而不用关注特定的领域模型。就本例而言,DefaultFooService 类在每个实现方法的主体中抛出 UnsupportedOperationException 实例这一事实是好的。这种行为让你看到事务被创建,然后回滚以响应 UnsupportedOperationException 实例。下面的列表显示了 FooService 的接口:
// 我们希望实现事务化的服务接口
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;
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)
必须在具有读写语义的事务中运行。下面几段将详细解释下面的配置:
<!-- 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">
<!-- 这是我们想让事务化的服务对象 -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- 事物 advice (发生了什么?看下面的 <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 接口定义的操作时,上述事务性 advice 都能运行。
FooService 接口所定义的操作的任何执行,都会运行上述事物 advice -->
<aop:config>
<!-- 切入点定义: 这里声明了拦截 FooService 中的所有方法 -->
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<!-- 顾问定义:advice 和 切入点注入 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>
<!-- 不要忘记数据源 -->
<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>
<!-- other <bean/> definitions here -->
</beans>
总结下:
- 配置数据源
- 配置事务管理器:持有数据源
- 配置事务 advice:持有事物管理器
- 配置 aop,声明顾问:持有切入点和 事物 advice
这样一来,被切入点切到的方法都会被这个事务 advice 增强。
检查一下前面的配置。它假定你想让一个服务对象(fooService Bean)成为事务性的。要应用的事务语义被封装在 <tx:advice/>
定义中。<tx:advice/>
定义如下:「所有以 get 开头的方法都要在只读事务的上下文中运行,所有其他方法都要以默认的事务语义运行」。<tx:advice/>
标签的 transaction-manager 属性被设置为将要驱动事务的 TransactionManager Bean 的名字(在本例中,是 txManager Bean)。
:::tips
如果你想接入的 TransactionManager 的 bean 名称是 transactionManager,你可以在事务 advice(<tx:advice/>
)中省略 transaction-manager 属性。如果你想接入的 TransactionManager Bean 有任何其他名字,你必须明确地使用 transaction-manager 属性,就像前面的例子一样。
:::
<aop:config/>
定义确保由 txAdvice Bean 定义的事务 advice 在程序中的适当点运行。首先,你定义一个与 FooService 接口(fooServiceOperation)中定义的任何操作的执行相匹配的 pointcut。然后,你通过使用顾问将该点切与 txAdvice 联系起来。结果表明,在执行 fooServiceOperation 时,由 txAdvice 定义的 advice 被运行。
在 <aop:pointcut/>
元素中定义的表达式是 AspectJ 的 pointcut 表达式。关于 Spring 中点式表达式的更多细节,请参见 AOP 部分。
一个常见的要求是使 整个服务层 具有事务性。做到这一点的最好方法是改变切入点表达式以匹配你的服务层中的任何操作。下面的例子展示了如何做到这一点:
<aop:config>
<!-- 不再局限于某一个类,而是整个 service 包 -->
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
现在我们已经分析了配置,你可能会问自己,「这些配置到底是做什么的?」
前面显示的配置是用来在从 fooService Bean 定义中创建的对象周围创建一个事务代理。该代理被配置了事务 advice,因此,当代理上有适当的方法被调用时,事务会被启动、暂停、标记为只读等等,这取决于与该方法相关的事务配置。考虑一下下面的程序,它测试了前面所示的配置:
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());
}
}
运行前述程序的输出结果应该类似于以下内容(为了清晰起见,DefaultFooService 类的 insertFoo(.)
方法抛出的 UnsupportedOperationException 的 Log4J 输出和堆栈跟踪被截断了)。
# Spring 容器正在启动.....
# 为 fooService 创建了隐式代理,0 个普通拦截器,1 个特定的拦截器
[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
# 事务 advice 在这里被启动了
# 创建了一个名为 [x.y.service.FooService.insertFoo] 的事务
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
# 为 JDBC 事物获得链接
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
# DefaultFooService 的 insertFoo(.) 方法抛出了一个异常......
# 应用规则来确定事务是否应该在 UnsupportedOperationException 时回滚
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
# 对 insertFoo 执行事务回滚
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
# 并且事务被回滚(默认情况下,RuntimeException 实例会导致回滚
# 回滚链接上的 JDBC 事务
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
# 释放 JDBC 链接
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
# 返回 JDBC 链接到数据源(应该就是连接池的操作)
[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)
为了使用反应式事务管理,代码必须使用反应式类型。 :::info Spring Framework 使用 ReactiveAdapterRegistry 来确定一个方法的返回类型是否是反应式的。 :::
下面的列表显示了之前使用的 FooService 的修改版本,但这次代码使用了反应式类型:
// 我们想把反应式服务接口变成事务性的。
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;
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 可以发射数据,但不一定完成。因此,依赖于整个事务成功完成的方法需要在调用代码中确保完成和缓冲结果。