Loading...

文章背景图

Spring AOP 中 @After 先于 @AfterReturning 执行的问题复现与分析

2026-06-02
4
-
- 分钟
|

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. 原因分析

不必深挖源码,理解到下面几个层面就足够:

  1. @After 的「最终通知」不等于「所有通知里最后执行」
    它的含义是「目标方法结束之后一定会执行」,对应 try-finally 中的 finally 语义;它和其它通知之间的相对顺序取决于拦截器在调用链中的位置。

  2. Spring AOP 底层是代理对象 + 拦截器链
    每一个 advice 都被包装成一个 MethodInterceptor,链式调用,不是按注解的中文译名(前置 / 后置 / 异常 / 最终)线性排队执行的。

  3. @After 类似 finally
    它通过 AspectJAfterAdvice 实现,本质上是在拦截器内部包了一层 try { proceed() } finally { ... }。如果它的拦截器位于 @AfterReturning 的内层,则它的 finally 会先于外层的「读取返回值」回调执行。

  4. 5.2.7 之前同一个 @Aspect 类内 advice 排序不可靠
    多 advice 之间需要一个排序规则,以决定谁在外层、谁在内层。在 5.2.7 之前的实现中,同一个切面类内 多个 advice 的相对顺序部分依赖了 Class#getDeclaredMethods() 的返回顺序——而 Java 7 起,这个方法明确不再保证返回顺序。

  5. 结果就是「内外层反过来」
    一旦 @After 拦截器恰好排在 @AfterReturning 之内(也就是更靠近目标方法那一层),它的 finally 就会比 @AfterReturning 更早完成,呈现为 close → commit 的反直觉输出。

跨切面类之间的顺序由 @OrderOrdered 接口决定,本文讨论的是「同一个 @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/rollbackclose 之前发生。

    对应的修复随 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 调用顺序的可靠性问题,正常情况下能看到 commitclose 之前。

  • 但即使新版本「修好了」,也不要把事务拆成多个普通通知。这种顺序是 Spring 内部的实现约定,不是契约;切面跨类组合、@Order 介入之后仍然会有出乎意料的情形。

可以在测试里打印一行版本号,确认你跑的到底是哪个 Spring:

import org.springframework.core.SpringVersion;

System.out.println(SpringVersion.getVersion());

10. 总结

  1. @After 的「最终通知」是 finally 语义,不是所有通知中最后一个执行的那个。

  2. Spring AOP 是拦截器链嵌套,不是按注解中文名线性排队。

  3. Spring 5.2.7 之前,同一个 @Aspect 类内多个 advice 的相对顺序可能不可靠。

  4. 官方在 Issue #25186 中说明并修复了该问题。

  5. 教学演示如果要手写「模拟事务」,请使用 @Around + try-catch-finally,顺序由 Java 语义保证。

  6. 真实业务代码请使用 @Transactional + @EnableTransactionManagement,把事务这件事交给 Spring。

文章目录