1. 问题背景
最近在学习 Spring AOP 时,我尝试用四种普通通知拼出一个「教学版的事务管理器」:
@Before:开启事务@AfterReturning:提交事务@AfterThrowing:回滚事务@After:关闭连接
按照「最终通知最后执行」这种字面理解,期望的输出应该是:
开启事务 → 执行业务方法 → 提交事务 → 关闭连接
但是实际跑出来的顺序是:
开启事务 → 执行业务方法 → 关闭连接 → 提交事务
close() 居然出现在了 commit() 之前。如果换成真实的数据库连接,连接已经被归还给连接池后再去 commit,提交基本就等同于无效操作。
后来翻官方仓库才发现,这个现象和 Spring 5.2.7 之前 同一个 @Aspect 类内多个 advice 顺序不可靠 是同一个问题,对应官方 Issue spring-projects/spring-framework#25186。本文记录一下完整的复现过程、原因分析和推荐写法。
2. 最小复现 Demo
不连真实数据库,只用一个 MockTxUtils 打印事务动作,便于直观看顺序。
2.1 AccountService
public interface AccountService {
void save();
}
2.2 AccountServiceImpl
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Override
public void save() {
System.out.println("===== 执行业务方法 save =====");
}
}
2.3 MockTxUtils
public class MockTxUtils {
public static void startTransaction() {
System.out.println("===== 开启事务 =====");
}
public static void commit() {
System.out.println("===== 提交事务 =====");
}
public static void rollback() {
System.out.println("===== 回滚事务 =====");
}
public static void close() {
System.out.println("===== 关闭连接 =====");
}
}
2.4 SplitTxAdvice(错误写法)
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class SplitTxAdvice {
@Pointcut("execution(public void com.example.afterdemo.AccountServiceImpl.save(..))")
public void pt() {}
@Before("pt()")
public void before() {
MockTxUtils.startTransaction();
}
@AfterReturning("pt()")
public void afterReturning() {
MockTxUtils.commit();
}
@AfterThrowing("pt()")
public void afterThrowing() {
MockTxUtils.rollback();
}
@After("pt()")
public void after() {
MockTxUtils.close();
}
}
2.5 SpringConfig
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.example.afterdemo")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2.6 测试代码
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AfterOrderTest {
@Test
public void testAdviceOrder() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = context.getBean(AccountService.class);
System.out.println("代理对象类型:" + accountService.getClass());
accountService.save();
context.close();
}
}
包名和切点表达式请按自己实际项目调整,例如我项目里实际是
com.qcbyjy.afterdemo。
3. 实际输出
在 Spring 5.2.7 之前的版本(例如 Spring Boot 2.3.0.RELEASE 对应的 Spring 5.2.6.RELEASE),可以看到:
===== 开启事务 =====
===== 执行业务方法 save =====
===== 关闭连接 =====
===== 提交事务 =====
注意第 3、4 行:close() 出现在 commit() 之前。
4. 为什么这会导致事务问题
如果是真实数据库连接,事务的正确收尾顺序应当是:
startTransaction()
执行业务 SQL
commit() / rollback()
close()
把 close() 放到 commit() 之前,可能引发的后果:
连接已被关闭或归还连接池,后续
commit()抛SQLException,事务实际未提交。在使用本地事务(
Connection#commit)时,连接关闭通常意味着这一段操作以默认行为结束(多数实现下表现为回滚),与代码层面的「正常提交」语义错位。错误日志和实际行为对不上:日志里印着「事务提交成功」,但数据库里数据并未落库。
这里只能说可能,因为具体行为还和连接池实现、数据库驱动有关。但无论怎样,这个顺序在生产环境都不能接受。
5. 原因分析
不必深挖源码,理解到下面几个层面就足够:
@After的「最终通知」不等于「所有通知里最后执行」
它的含义是「目标方法结束之后一定会执行」,对应 try-finally 中的finally语义;它和其它通知之间的相对顺序取决于拦截器在调用链中的位置。Spring AOP 底层是代理对象 + 拦截器链
每一个 advice 都被包装成一个MethodInterceptor,链式调用,不是按注解的中文译名(前置 / 后置 / 异常 / 最终)线性排队执行的。@After类似 finally
它通过AspectJAfterAdvice实现,本质上是在拦截器内部包了一层try { proceed() } finally { ... }。如果它的拦截器位于@AfterReturning的内层,则它的finally会先于外层的「读取返回值」回调执行。5.2.7 之前同一个
@Aspect类内 advice 排序不可靠
多 advice 之间需要一个排序规则,以决定谁在外层、谁在内层。在 5.2.7 之前的实现中,同一个切面类内 多个 advice 的相对顺序部分依赖了Class#getDeclaredMethods()的返回顺序——而 Java 7 起,这个方法明确不再保证返回顺序。结果就是「内外层反过来」
一旦@After拦截器恰好排在@AfterReturning之内(也就是更靠近目标方法那一层),它的 finally 就会比@AfterReturning更早完成,呈现为close → commit的反直觉输出。
跨切面类之间的顺序由
@Order或Ordered接口决定,本文讨论的是「同一个@Aspect类内」的情况。
6. 官方证据
这一节的内容仅做引用与转述,不复制原文。
Spring Framework Issue #25186 — Implement reliable invocation order for advice within an @Aspect class
链接:https://github.com/spring-projects/spring-framework/issues/25186这条 Issue 的核心讯息有三点:
目标:为同一个
@Aspect类内的多个 advice 实现可靠的调用顺序。背景:旧实现的排序在某些 JDK 上不稳定,与反射方法返回顺序无保证有关。
修复后效果:同一个
@Aspect类中@AfterReturning/@AfterThrowing在调用顺序上会先于@After,从而让commit/rollback在close之前发生。
对应的修复随 Spring Framework 5.2.7 一同发布。
Spring 官方文档「Declaring Advice」一节也对各类通知的语义做了说明:
https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/advice.html
重要提醒:Issue #25186 的修复只解决了「顺序可靠性」问题,并不等于官方推荐用拆分通知的方式管理事务。事务管理仍应使用
@Transactional或@Around。
7. 推荐修复方式一:使用 @Around 模拟事务(教学场景)
如果是教学演示、想直观地看到事务流程,最稳妥的方式是把所有事务动作收敛到一个 @Around 通知里,靠 Java 自身的 try-catch-finally 保证顺序:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AroundTxAdvice {
@Pointcut("execution(public void com.example.afterdemo.AccountServiceImpl.save(..))")
public void pt() {}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
MockTxUtils.startTransaction();
Object result = pjp.proceed();
MockTxUtils.commit();
return result;
} catch (Throwable e) {
MockTxUtils.rollback();
throw e;
} finally {
MockTxUtils.close();
}
}
}
pjp.proceed() 才是真正执行目标方法的位置。正常路径下输出:
===== 开启事务 =====
===== 执行业务方法 save =====
===== 提交事务 =====
===== 关闭连接 =====
业务方法抛异常时输出:
===== 开启事务 =====
===== 执行业务方法 save =====
===== 回滚事务 =====
===== 关闭连接 =====
这种顺序由 Java 语义本身保证,不依赖拦截器拓扑结构、也不受 Spring 版本影响。
8. 推荐修复方式二:实际开发使用 @Transactional
真实项目中不要手写 AOP 来管理事务。Spring 已经为这件事做好了最佳实践——声明式事务:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountServiceImpl implements AccountService {
@Override
@Transactional
public void save() {
// 业务逻辑
}
}
配置事务管理器:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
并在配置类上开启:
@EnableTransactionManagement
@Transactional 背后实际上就是一个 Spring 自带的环绕拦截器(TransactionInterceptor),它会负责:
从数据源获取连接
开启事务
把连接绑定到当前线程(
TransactionSynchronizationManager)正常时提交
异常时回滚
最终释放连接、解绑线程
写业务代码的人只需要标一个注解就够了——这正是声明式事务相对手写 AOP 的最大价值。
9. 版本提醒
如果你用的是 Spring 5.2.7 之前 的版本,更容易复现本文描述的 advice 顺序问题。
如果你用的是 Spring 5.2.7 及之后 的版本,官方已经修复同一个
@Aspect类内 advice 调用顺序的可靠性问题,正常情况下能看到commit在close之前。但即使新版本「修好了」,也不要把事务拆成多个普通通知。这种顺序是 Spring 内部的实现约定,不是契约;切面跨类组合、
@Order介入之后仍然会有出乎意料的情形。
可以在测试里打印一行版本号,确认你跑的到底是哪个 Spring:
import org.springframework.core.SpringVersion;
System.out.println(SpringVersion.getVersion());
10. 总结
@After的「最终通知」是 finally 语义,不是所有通知中最后一个执行的那个。Spring AOP 是拦截器链嵌套,不是按注解中文名线性排队。
在 Spring 5.2.7 之前,同一个
@Aspect类内多个 advice 的相对顺序可能不可靠。官方在 Issue #25186 中说明并修复了该问题。
教学演示如果要手写「模拟事务」,请使用
@Around+try-catch-finally,顺序由 Java 语义保证。真实业务代码请使用
@Transactional+@EnableTransactionManagement,把事务这件事交给 Spring。