不仅仅是事务通知 (Advising Transactional Operations)
假设你希望同时运行事务性操作和一些基本的性能分析(profiling)通知。如何在 <tx:annotation-driven/> 的上下文中实现这一点?
当你调用 updateFoo(Foo) 方法时,你希望看到以下动作序列:
- 配置的性能分析切面启动。
- 事务通知运行(开启事务)。
- 受通知对象上的方法运行。
- 事务提交。
- 性能分析切面报告整个事务性方法调用的确切持续时间。
注意
本章不打算详细解释 AOP(除非它适用于事务)。有关 AOP 配置和一般 AOP 的详细介绍,请参阅 AOP。
下面的代码展示了前面讨论的简单性能分析切面:
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;
public class SimpleProfiler implements Ordered {
private int order;
// 允许我们控制通知的顺序
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
// 这个方法是环绕通知
public Object profile(ProceedingJoinPoint call) throws Throwable {
Object returnValue;
StopWatch clock = new StopWatch(getClass().getName());
try {
clock.start(call.toShortString());
returnValue = call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
return returnValue;
}
}package x.y
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
import org.springframework.core.Ordered
class SimpleProfiler : Ordered {
private var order: Int = 0
// 允许我们控制通知的顺序
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
// 这个方法是环绕通知
fun profile(call: ProceedingJoinPoint): Any {
var returnValue: Any
val clock = StopWatch(javaClass.name)
try {
clock.start(call.toShortString())
returnValue = call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
return returnValue
}
}通知的顺序是通过 Ordered 接口控制的。有关通知排序的完整细节,请参阅通知排序 (Advice ordering)。
以下配置创建了一个 fooService bean,并按照期望的顺序对其应用了性能分析和事务切面:
<?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"/>
<!-- 这是切面 -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- 在事务通知之前运行(因此顺序号较低) -->
<property name="order" value="1"/>
</bean>
<tx:annotation-driven transaction-manager="txManager" order="200"/>
<aop:config>
<!-- 此通知在事务通知周围运行 -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</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>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>你可以以类似的方式配置任意数量的额外切面。
以下示例创建了与前两个示例相同的设置,但使用了纯 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"/>
<!-- 性能分析通知 -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- 在事务通知之前运行(因此顺序号较低) -->
<property name="order" value="1"/>
</bean>
<aop:config>
<aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
<!-- 在性能分析通知之后运行(参见 order 属性) -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
<!-- order 值高于性能分析切面 -->
<aop:aspect id="profilingAspect" ref="profiler">
<aop:pointcut id="serviceMethodWithReturnValue"
expression="execution(!void x.y..*Service.*(..))"/>
<aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
</aop:aspect>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 此处为其他 <bean/> 定义,例如 DataSource 和 TransactionManager -->
</beans>上述配置的结果是一个 fooService bean,其应用的性能分析和事务切面遵循该顺序(先性能分析,后事务)。如果你希望性能分析通知在进入时运行在事务通知之后,并在退出时运行在事务通知之前(即被包含在事务内部),你可以交换性能分析切面 bean 的 order 属性值,使其高于事务通知的 order 值。
你可以用类似的方式配置其他切面。
补充教学
1. AOP 顺序对于事务正确性的重要性
理解 AOP 顺序(Ordering)不仅仅是为了做日志,它关乎数据的正确性。 场景:你有一个重试切面(Retry Advice)和一个事务切面(Transaction Advice)。
- 正确顺序:Retry (Order=1) -> Transaction (Order=2)。
- 流程:Retry 捕获异常 -> 重新发起调用 -> 开启新事务。
- 结果:每次重试都是一个干净的新事务,数据一致。
- 错误顺序:Transaction (Order=1) -> Retry (Order=2)。
- 流程:开启事务 -> Retry 捕获异常 -> 重新发起调用。
- 结果:Retry 是在同一个已经失败(可能被标记为 RollbackOnly)的事务中重试的。这会导致“不可重复读”或直接抛出
UnexpectedRollbackException,重试失效。
2. "洋葱模型" 可视化
Spring AOP 的执行就像剥洋葱:
- Method Entry (入参):Order 值越小,越先执行(在外层)。
- Method Exit (出参):Order 值越小,越后执行(在外层)。
所以在本例中: Profiler (Order=1) Start Transaction (Order=2) Start Service Method Transaction (Order=2) Commit Profiler (Order=1) Stop -> 打印总耗时(包含事务提交的时间)
3. 注解配置中的 Order
在 Spring Boot 中,你通常通过注解来控制:
@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE)(默认为 Integer.MAX_VALUE,意味着事务通常在最内层)- 如果你写了一个
@Aspect并想包裹事务,只需让你的切面实现Ordered接口并返回一个比MAX_VALUE小的值即可。