← 返回题目列表

多个代理或多个切面同时存在时,执行顺序如何控制?

高频 困难 第 13 / 25 题 更新于 2026/08/01
代理模式Spring AOP切面顺序拦截器链

简化版

多个代理或切面同时存在时,本质会形成一条拦截器链,外层切面先进入、后退出。Spring 中通常用 @OrderOrdered 或配置顺序控制切面优先级;顺序会影响事务、权限、缓存、日志和异常处理的语义,不能只看代码是否执行。

详细版

代理增强不是简单地“都执行一遍”,而是按链式结构嵌套执行:

AdviceA before
  AdviceB before
    Target method
  AdviceB after
AdviceA after

如果有权限、事务、缓存、日志 4 个切面,顺序不同会导致完全不同的行为。例如权限应该尽量在事务前拦截,避免无权限请求还开启事务;事务通常要包住数据库写入;日志或监控要看是记录整体耗时还是只记录业务方法耗时。

Spring 里可以通过:

  • @Order(1)
  • 实现 Ordered
  • 配置 Advisor 顺序;
  • 明确切点粒度;
  • 避免多个切面互相吞异常;

来控制执行顺序。面试要强调:顺序控制不是为了美观,而是为了保证横切逻辑的语义正确。

完整版教学

一、多个切面为什么会形成调用链

代理模式的增强逻辑通常不是平铺执行,而是包裹式执行。每个切面都像一层包装,调用进入时按外到内执行 before,调用返回或抛异常时按内到外执行 after 或异常处理。

Client
  -> Proxy/Advice A
     -> Proxy/Advice B
        -> Proxy/Advice C
           -> Target

因此多个切面的顺序不只是列表顺序,而是嵌套边界。最外层切面能看到整个调用耗时,内层切面只能看到它内部的那段逻辑。这个差异会影响监控指标、事务范围、异常捕获和缓存命中统计。

记忆钩子:切面顺序不是排队执行,而是洋葱式嵌套,先进的后出。

二、用权限、事务、日志看顺序差异

假设一个下单方法同时有权限校验、事务和日志监控。比较合理的顺序通常是:日志在最外层记录整体耗时,权限在事务前拒绝非法请求,事务包住真正写库逻辑。

推荐顺序:
1. 日志/Trace before
2. 权限 before
3. 事务 begin
4. 目标方法写库
5. 事务 commit/rollback
6. 权限 after
7. 日志/Trace after

如果事务放在权限外面,无权限请求也可能开启事务。一次请求影响不大,但高并发下会造成无意义的连接占用。假设每秒 1000 个无权限请求,每个都先占用数据库连接 5 ms,就会产生 1000 * 5ms = 5000ms 的连接占用时间,等价于持续占用约 5 个连接。

顺序不是小细节,它会影响资源使用和失败语义。

三、Spring 中如何指定顺序

Spring AOP 中常用 @OrderOrdered 控制切面顺序。数值越小,优先级越高,通常越靠外层,进入时越早执行,退出时越晚执行。

@Aspect
@Order(1)
@Component
public class TraceAspect {
    @Around("execution(* com.example..*(..))")
    public Object trace(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            System.out.println(System.nanoTime() - start);
        }
    }
}

@Aspect
@Order(2)
@Component
public class PermissionAspect {
    @Around("@annotation(RequiresPermission)")
    public Object check(ProceedingJoinPoint pjp) throws Throwable {
        // check permission
        return pjp.proceed();
    }
}

如果 TraceAspect 的 order 更小,它会先进入、最后退出,记录的是包含权限和目标方法在内的整体耗时。

四、缓存和事务顺序为什么很敏感

缓存切面和事务切面顺序经常引发问题。比如查询方法上同时有缓存和事务,缓存命中时是否还要开启事务?如果缓存在事务外层,命中缓存可以直接返回,不开启事务;如果事务在缓存外层,即使缓存命中也可能先开启事务。

缓存在外:
  Cache before
    命中 -> 直接返回
    未命中 -> Transaction -> DB -> 写缓存

事务在外:
  Transaction begin
    Cache before
      命中 -> 返回
  Transaction commit

对只读查询来说,缓存在外通常更省资源。对写操作和缓存淘汰,要考虑事务提交成功后再删缓存或更新缓存,避免事务回滚但缓存已经变化。

场景顺序关注点原因
读缓存缓存尽量在事务外命中时少开事务
写库删缓存缓存动作要考虑事务提交避免回滚后缓存被误删
审计日志看是否要记录失败请求决定放在外层还是内层
权限校验通常早于事务无权限请求尽早拒绝

五、异常处理切面会改变链路语义

多个切面中最危险的是异常处理。如果某个切面吞掉异常并返回默认值,外层事务切面可能认为调用成功,从而提交事务。这会导致原本应该回滚的数据被提交。

@Around("@annotation(Fallback)")
public Object fallback(ProceedingJoinPoint pjp) {
    try {
        return pjp.proceed();
    } catch (Exception e) {
        return null; // 危险:吞异常
    }
}

如果这段 fallback 包在事务内部,目标方法抛出的异常被吞掉,事务边界可能看不到失败。面试里可以强调:异常切面必须明确哪些异常能降级、哪些必须继续抛出,尤其不能随便吞数据库写入异常。

危险链路:
  Transaction begin
    Fallback catches exception -> return null
  Transaction commit

异常顺序和异常传播,往往比 before/after 顺序更影响数据一致性。

六、多个代理对象叠加时怎么理解

有些系统不仅有 Spring AOP,还可能有 RPC 代理、Mapper 代理、事务代理、监控代理。调用方看到的是一个对象,但内部可能经过多层代理。

Controller
  -> Service AOP Proxy
     -> Transaction Interceptor
        -> MyBatis Mapper Proxy
           -> SQL Executor

每一层代理解决不同问题。Service 代理负责事务和业务切面,Mapper 代理负责把接口方法转成 SQL 执行,RPC 代理负责网络调用。排查问题时要明确当前讨论的是哪一层代理,否则容易把 MyBatis 的 Mapper 代理和 Spring AOP 代理混为一谈。

七、如何测试切面顺序

切面顺序不能只靠“感觉配置对了”。可以写小型集成测试记录执行顺序,或在关键切面里用日志/Trace 标记进入和退出。

期望日志:
Trace before
Permission before
Tx begin
Target
Tx commit
Permission after
Trace after

测试里可以用一个共享列表记录事件:

events.add("trace-before");
Object result = pjp.proceed();
events.add("trace-after");

对于事务、缓存、异常降级这类敏感切面,最好覆盖成功、失败、缓存命中、缓存未命中 4 类路径。只测成功路径,很容易漏掉异常传播顺序问题。

八、常见误区与追问

  • 误区:多个切面只要都执行,顺序不重要。 顺序会影响事务范围、缓存命中、异常传播和资源占用。
  • 误区:@Order 数字越大越先执行。 Spring 中通常数值越小优先级越高,进入时越早,退出时越晚。
  • 误区:日志切面永远应该最里面。 如果要记录整体耗时,日志或 Trace 往往应该在外层。
  • 追问:权限和事务谁先执行? 通常权限先于事务,非法请求尽早拒绝,减少无意义事务资源占用。
  • 追问:缓存和事务顺序怎么考虑? 读缓存可放事务外,写缓存或删缓存要考虑事务提交时机。
  • 追问:异常被切面吞掉有什么风险? 外层事务可能看不到异常而提交,导致数据一致性问题。
  • 追问:怎么验证切面顺序? 用集成测试、事件列表、Trace 日志验证 before/after/exception 路径。

九、加强记忆

切面顺序记住“外层先入后出”。权限尽量早拦,事务包住真正写库,缓存看命中和提交语义,日志看要统计整体还是局部;异常切面不能随意吞异常,因为它可能改变事务提交和回滚结果。