Spring 深度内核-核心容器与扩展机制-AOP 原理剖析:代理创建、拦截链与通知顺序

0 阅读21分钟

概述

在《IoC 设计哲学:容器、BeanDefinition 与配置元信息》《Bean 生命周期全景:从扫描到销毁的完整轨迹》及《依赖注入的精髓:@Autowired 与构造器注入的设计差异》中,我们已经完整窥探了 Spring IoC 容器如何装配 Bean,以及依赖注入如何满足对象间的协作需求。然而 Spring 的核心价值远不止于“对象工厂”,更在于它能以非侵入的方式为普通 Java 对象赋予横切关注点——这就是 AOP。本文将从代理的诞生开始,拆穿 Spring AOP 的“魔法”,揭示它不过是一套经过精密设计的 代理 + 拦截链 架构。

AOP 是面向对象编程的有力补充,它将散布在应用各处的横切关注点(日志、事务、安全等)模块化为切面,从而消除代码冗余。Spring AOP 在实现上选择了动态代理而非字节码增强,这既是与 IoC 容器无缝集成的工程权衡,也是理解源码的钥匙。本文将逐一拆解代理创建、拦截链、通知顺序三大板块,结合 Spring Framework 5.x 关键源码深入分析 Spring AOP 的工作原理,帮助专家级开发者在面试和事故排查中做到游刃有余。

  • 两种代理机制:JDK 动态代理(基于接口)与 CGLIB(基于子类)的决策源码与强制场景。
  • 拦截器链的构建:Advisor 的筛选、排序、适配,以及如何形成一个严格有序的调用链。
  • 拦截器链的执行ReflectiveMethodInvocation 通过递归调用实现责任链模式,是 AOP 的执行核心。
  • 通知顺序规则:五大通知类型有严格的排序规则,多个同类型通知亦有顺序,掌握它才能避免误用。
  • 生命周期中的时机:AOP 代理在 postProcessAfterInitialization 阶段创建,发生在初始化之后,这正是 @PostConstruct 中调用增强方法失效的根本原因。
graph TD
    subgraph A["① 概述与AOP概念"]
        A1["横切关注点"]
        A2["代理式AOP定位"]
    end
    subgraph B["② 代理创建机制"]
        B1["代理触发点"]
        B2["代理选择JDK与CGLIB"]
        B3["代理工厂与创建"]
    end
    subgraph C["③ 通知与拦截链构建"]
        C1["通知类型体系"]
        C2["Advisor筛选与排序"]
        C3["拦截器链适配"]
    end
    subgraph D["④ 拦截链执行与通知顺序"]
        D1["递归执行入口"]
        D2["ReflectiveMethodInvocation"]
        D3["五大通知顺序规则"]
    end
    subgraph E["⑤ 设计权衡"]
        E1["代理式与织入式"]
        E2["性能与限制"]
    end
    subgraph F["⑥ 生产事故"]
        F1["PostConstruct失效"]
        F2["self-invocation失效"]
        F3["Order冲突"]
        F4["final方法"]
    end
    subgraph G["⑦ 面试专题"]
        G1["原理与对比"]
        G2["系统设计题"]
    end
    A --> B --> C --> D --> E --> F --> G

上图以七个核心模块串联了本文的认知路径,层次递进、边界清晰:

  1. 概述与AOP概念
    从横切关注点出发,明确 Spring AOP 的代理式 AOP 定位。厘清 JoinPoint、Pointcut、Advice、Aspect、Advisor、Weaving 等核心术语及其关系,为后续源码分析奠定理论基础。

  2. 代理创建机制
    深入 Bean 生命周期中 postProcessAfterInitialization 这一精确节点,剖析 AbstractAutoProxyCreator 如何通过 wrapIfNecessary 判断是否需要代理,再借助 DefaultAopProxyFactory 的决策树选择 JDK 动态代理或 CGLIB 代理。完整展现一个原始 Bean 如何“穿上代理壳”的全流程。

  3. 通知与拦截链构建
    讲解 @Before@AfterReturning@AfterThrowing@After@Around 五种通知类型如何通过适配器模式转换为统一的 MethodInterceptor。在此基础上,拆解 DefaultAdvisorChainFactory 如何通过切点匹配、排序和适配,将分散的 Advisor 编织成一条严格有序的拦截器链。

  4. 拦截链执行与通知顺序
    ReflectiveMethodInvocation.proceed() 的递归调用为核心,揭示责任链模式如何通过“压栈-出栈”实现环绕增强效果。系统归纳五种通知类型的固定执行顺序,以及多个同类型通知时 @Before 升序、@After 降序的内在原理。

  5. 设计权衡
    从工程视角对比代理式与织入式 AOP 的取舍、JDK 代理与 CGLIB 的性能与限制,并简要讨论 Spring AOP 与声明式事务、异步等模块的协同关系,帮助读者建立完整的设计边界认知。

  6. 生产事故
    独立模块,用至少四个典型线上事故(@PostConstruct 失效、self‑invocation 失效、Order 冲突、final 方法限制),严格遵循“现象→排查→根源→解决→最佳实践”五步法拆解,让原理直接落地为排错能力。

  7. 面试专题
    严格与正文知识模块隔离,整理不少于 18 道高频面试题,含两道系统设计题。每道题提供标准回答、多角度追问和加分回答,源码细节、设计模式和事故教训贯穿其中。

整条路径遵循 “概念→机制→原理→权衡→实战→面试” 的递进逻辑,既保证了学术级的严谨推导,又兼顾了工业级的实战深度,使读者能够在脑海里构建出一张完整的 Spring AOP 知识网络。

一、AOP 概念与 Spring AOP 定位

1.1 核心概念体系

要理解 Spring AOP 的实现,必须首先精确把握 AOP 的几个核心概念。下图展示了 JoinPoint、Pointcut、Advice、Aspect、Advisor 以及 Weaving 之间的静态关系。

classDiagram
direction LR
class JoinPoint {
    +Object getThis()
    +Object[] getArgs()
    +Method getMethod()
}
class Pointcut {
    +ClassFilter getClassFilter()
    +MethodMatcher getMethodMatcher()
}
class Advice {
    <<interface>>
}
class MethodBeforeAdvice {
    +before(Method, Object[], Object)
}
class AfterReturningAdvice {
    +afterReturning(Object, Method, Object[], Object)
}
class ThrowsAdvice {
    <<marker>>
}
class MethodInterceptor {
    +Object invoke(MethodInvocation)
}
class Advisor {
    +Advice getAdvice()
}
class PointcutAdvisor {
    +Pointcut getPointcut()
}
class Aspect {
    <<module>>
}
class Weaving {
    <<process>>
}

JoinPoint -- Pointcut : "匹配"
Aspect ..> Advice : 包含
Aspect ..> Pointcut : 包含
Advisor <|.. PointcutAdvisor : 组合
PointcutAdvisor o-- Pointcut
Advisor o-- Advice
MethodBeforeAdvice --|> Advice
AfterReturningAdvice --|> Advice
ThrowsAdvice --|> Advice
MethodInterceptor --|> Advice
Weaving ..> JoinPoint : 在连接点织入
Weaving ..> Advisors : 应用

主旨概括:本图刻画了 AOP 概念体系中各个抽象元素的依赖与组合关系,从切面到切点再到通知器。 分解

  • JoinPoint 表示程序执行过程中的一个具体点,如方法调用、异常抛出。Spring AOP 仅支持方法级别的连接点。
  • Pointcut 定义了一组连接点的过滤规则,通过 ClassFilterMethodMatcher 来确定哪些 JoinPoint 需要增强。
  • Advice 是增强行为的抽象,Spring 将其分为 MethodBeforeAdviceAfterReturningAdviceThrowsAdviceMethodInterceptor
  • Advisor 是 Spring 特有的概念,将 Advice 与 Pointcut 组合为一个“通知器”。PointcutAdvisor 是最常用的实现形式。
  • Aspect 是更高层次的模块化单元,通常用 @Aspect 注解的类表示,内部可定义多个 Advice 与 Pointcut。
  • Weaving 表示将切面逻辑插入目标对象的过程,Spring AOP 选择运行时动态代理织入。 原理:Spring AOP 容器在 Bean 初始化阶段,将所有 @Aspect 类解析为一组 Advisor,并将这些 Advisor 注册到 AdvisedSupport 中。当需要创建代理时,根据当前 Bean 和方法匹配 Advisor 的 Pointcut,若命中则构建拦截器链并执行。 工程结论:理解这几个概念的关系,是读懂 AOP 源码和排查 AOP 相关问题的基本前提。特别要注意 Spring 中的 Advisor 本质上是“切点+通知”的复合体,而非一个单纯的 Advice。

1.2 Spring AOP 的定位

Spring AOP 是基于代理的“框架级” AOP 实现。它在运行时为 Bean 创建代理对象,将增强逻辑织入代理而非原始类。这与编译期或类加载期织入的 AspectJ 截然不同。

  • 代理式 AOP:Spring AOP 通过 BeanPostProcessor 在 Bean 生命周期的 postProcessAfterInitialization 阶段,将符合条件的 Bean 包装成代理对象。因此它只能拦截从容器外部调用代理对象的方法,无法拦截 Bean 内部的自调用(this.method()),因为 this 是原始对象,绕过代理。
  • 与 IoC 容器的关系:AOP 并不是独立于容器之外的增强层,而是深深嵌入了容器的生命周期。容器在完成 Bean 的依赖注入和初始化后,再通过 AbstractAutoProxyCreator 为 Bean 罩上一层代理壳。换言之,AOP 是容器在 Bean 后期进行的一种隐式增强
  • 术语映射:Spring 通过 @EnableAspectJAutoProxy<aop:aspectj-autoproxy/> 激活对 @AspectJ 注解的支持。AnnotationAwareAspectJAutoProxyCreator 会扫描容器中所有带有 @Aspect 的 Bean,解析其切点表达式和通知方法,生成一个或多个 AspectJPointcutAdvisor,并将它们加入到全局 Advisor 注册表中。

这种设计使得 Spring AOP 与 IoC 容器天然集成,开发者只需关注切面编写,无需关心织入过程。但代价是不能拦截内部调用,且代理对象可能影响类型判断(如 bean.getClass() 返回代理类名)。这些工程限制将在后续模块中详细讨论。

二、代理创建的决策与流程

AOP 代理的创建发生在 Bean 生命周期中一个非常精确的节点:initializeBean 完成之后、postProcessAfterInitialization 执行期间。本章我们沿触发点 → 是否需要代理判断 → 代理工厂选择 → 具体代理创建这条路径,深入 Spring 5.x 源码,彻底揭示代理诞生的每一个细节。

2.1 触发点:AbstractAutoProxyCreator.postProcessAfterInitialization

AbstractAutoProxyCreator 是 Spring AOP 代理创建的核心后置处理器。其 postProcessAfterInitialization 方法在 Bean 已经完全初始化(即属性填充、初始化回调均已执行完毕)后被调用。

// AbstractAutoProxyCreator.java (Spring Framework 5.x 核心方法)
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 主要逻辑:根据需要包装 Bean
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

解读:该方法首先处理“早期代理引用”的缓存,避免对同一个 Bean 重复创建代理。随后调用 wrapIfNecessary 决定是否需要创建代理。注意此时 Bean 本身已经是完全初始化的 原始对象,并非代理。该方法返回的对象即为最终放入容器的实例——可能是原始 Bean(若无需增强),也可能是新创建的代理对象。

2.2 是否创建代理:wrapIfNecessary

wrapIfNecessary 是真正的决策与创建入口,它负责汇总与当前 Bean 匹配的 Advisor,并决定是否启动代理创建。

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 如果已经是被通知的 Bean 或提前返回的特定 Bean 则跳过
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    // 获取匹配当前 Bean 的 Advisor 数组
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 正式创建代理
        Object proxy = createProxy(
                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

解读

  • 短路机制:Spring 首先利用 advisedBeans 缓存已经判断无需增强的 Bean;随后过滤基础设施类(如 AdviceAdvisor 自身)和自定义跳过逻辑(shouldSkip)。
  • 匹配 Advisor:核心调用 getAdvicesAndAdvisorsForBean。该方法在 AbstractAdvisorAutoProxyCreator 中实现,会遍历容器中所有的 Advisor(包括 @Aspect 解析出的 Advisor),通过 PointcutAdvisor.getPointcut().getClassFilter()getMethodMatcher() 检查是否与当前 Bean 的类及方法匹配。若匹配,则收集此 Advisor。
  • 决策:若存在任何一个匹配的 Advisor,则 specificInterceptors 不为空,进入代理创建阶段;否则标记为无需代理并直接返回原始 Bean。

2.3 代理工厂的选择:DefaultAopProxyFactory.createAopProxy

当决定创建代理后,Spring 将任务委托给 AopProxyFactoryDefaultAopProxyFactory 根据配置的 proxyTargetClass 属性以及被代理类是否实现了接口,选择 JDK 动态代理或 CGLIB 代理。

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config); // CGLIB 代理
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }

    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] ifcs = config.getProxiedInterfaces();
        return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
    }
}

解读

  • 决策树
    1. 如果 config.isOptimize() 为 true,或者 config.isProxyTargetClass() 为 true,或者目标类没有提供任何用户接口(只有 SpringProxy 标记接口),则倾向于使用 CGLIB。
    2. 然而,即使优先 CGLIB,如果 targetClass 本身是接口或者已经是 java.lang.reflect.Proxy,则必须回退到 JDK 动态代理。
    3. 如果上述条件都不满足,即目标类实现了用户接口且未强制 proxyTargetClass,则使用 JDK 动态代理。
  • Spring Boot 为何默认 CGLIB:Spring Boot 2.x 中通过 AopAutoConfiguration 自动配置了 spring.aop.proxy-target-class=true,即 proxyTargetClass 默认为 true。主要原因包括:
    • 避免强制开发者必须面向接口编程,更符合 Spring Boot 开箱即用的哲学。
    • 在依赖注入场景下,若期望按类型注入实现类,JDK 动态代理生成的 $Proxy 类无法注入到具体实现类类型,容易引发 NoSuchBeanDefinitionException
    • CGLIB 可以代理未实现接口的普通类,提供了更广泛的代理能力。

2.4 JDK 动态代理的创建:JdkDynamicAopProxy

当决策使用 JDK 动态代理时,JdkDynamicAopProxy.getProxy() 通过 JDK 的 Proxy.newProxyInstance 创建代理。

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
    if (logger.isTraceEnabled()) {
        logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
    }
    Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
    findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
    return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

解读:代理对象持有 AdvisedSupport 配置(其中包含所有 Advisor),同时将 JdkDynamicAopProxy 自身作为 InvocationHandler 传入。当代理对象上的任何方法被调用时,都会进入 invoke 方法,从而被拦截器链处理。

2.5 CGLIB 代理的创建:CglibAopProxy

CGLIB 代理的创建更为复杂,它通过继承目标类生成子类,并注册 DynamicAdvisedInterceptor 回调。

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
    // ...
    // 创建 Enhancer
    Enhancer enhancer = createEnhancer();
    if (classLoader != null) {
        enhancer.setClassLoader(classLoader);
    }
    enhancer.setSuperclass(this.advised.getTargetClass());
    enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
    enhancer.setCallbackTypes(new Class[]{
            DynamicAdvisedInterceptor.class, NoOp.class, TargetInterceptor.class});
    // 创建代理子类
    Class<?> proxyClass = enhancer.createClass();
    // ...
    // 实例化代理对象并设置拦截器
    CglibAopProxy.advisedDispatcher = this.advised;
    enhancer.setCallbacks(new Callback[]{
            new DynamicAdvisedInterceptor(this.advised),
            NoOp.INSTANCE,
            new TargetInterceptor(this.advised)});
    return enhancer.create();
}

解读:CGLIB 通过 ASM 库在运行时生成目标类的子类,重写所有非 final 方法,在方法内调用注册的 DynamicAdvisedInterceptor.intercept。这样即使目标类没有实现接口,也能被增强。但正因为是继承,final 方法和 final 类无法被增强,这是 CGLIB 代理的一个关键限制。

代理创建决策流程图

flowchart TD
    A[Bean初始化完成] --> B[postProcessAfterInitialization]
    B --> C{wrapIfNecessary<br/>判断是否需要代理}
    C -->|无匹配Advisor| D[返回原始Bean]
    C -->|有匹配Advisor| E[createProxy]
    E --> F{DefaultAopProxyFactory<br/>选择代理类型}
    F -->|接口充足且未强制CGLIB| G[JdkDynamicAopProxy]
    F -->|未实现接口或proxyTargetClass=true| H[CglibAopProxy]
    G --> I[Proxy.newProxyInstance<br/>生成JDK代理]
    H --> J[Enhancer.create<br/>生成CGLIB子类代理]
    I --> K[代理对象持有AdvisedSupport]
    J --> K
    K --> L[返回代理对象放入容器]

主旨概括:本图完整展示了从 Bean 初始化完毕到最终代理对象返回容器的全部分支。 分解postProcessAfterInitialization 触发 wrapIfNecessary,该方法通过 Advisor 匹配决定是否代理;若需要,则 DefaultAopProxyFactory 根据接口情况和 proxyTargetClass 属性选择 JDK 或 CGLIB 代理技术;两种技术最终都生成一个包含拦截器链能力的代理对象。 原理:整个决策树将“是否需要代理”与“如何代理”解耦,符合开闭原则。AdvisedSupport 作为配置信息载体,在代理对象内部被复用。 工程结论:理解该流程有助于分析为何某些 Bean 变成代理,以及为何某些情况代理不生效。

三、通知类型与拦截器的适配

Spring AOP 定义了五种常用通知类型,但拦截器链要求执行单元是统一的 MethodInterceptor 接口。因此 Spring 内部通过适配器模式将不同类型的 Advice 转换为 MethodInterceptor

  • @BeforeAspectJMethodBeforeAdvice,适配为 MethodBeforeAdviceInterceptor
    public class MethodBeforeAdviceInterceptor implements MethodInterceptor {
        private final MethodBeforeAdvice advice;
        @Override
        public Object invoke(MethodInvocation mi) throws Throwable {
            this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
            return mi.proceed();
        }
    }
    
  • @AfterReturningAspectJAfterReturningAdvice,适配为 AfterReturningAdviceInterceptor,其 invokeproceed() 返回后执行通知逻辑。
  • @AfterThrowingAspectJAfterThrowingAdvice,适配为 ThrowsAdviceInterceptor,在 proceed() 抛出异常时执行。
  • @AfterAspectJAfterAdvice,适配为 AfterReturningAdviceInterceptor 类似,但不论正常还是异常都会执行(本质通过 finally 机制)。
  • @AroundAspectJAroundAdvice,自身直接实现 MethodInterceptor,无需适配。

为何统一为 MethodInterceptor:拦截器链的实现采用的是责任链模式,每个节点都是 MethodInterceptor,通过调用 invoke(MethodInvocation) 并内部回调 MethodInvocation.proceed() 实现整个链的串联。统一接口省去了类型判断和分支逻辑,是设计上的精巧抽象。

四、拦截器链的构建

当代理对象上的方法被调用时,需要动态地根据当前方法从 AdvisedSupport 中筛选出匹配的 Advisor,适配成 MethodInterceptor,并进行排序,形成拦截器链。这个过程由 DefaultAdvisorChainFactory 负责。

4.1 构建序列图

sequenceDiagram
    participant Proxy as 代理对象
    participant Jdk/CGLIB as 代理调用入口
    participant ChainFactory as DefaultAdvisorChainFactory
    participant Registry as AdvisorAdapterRegistry
    participant AllAdvisors as 全局Advisor列表
    Proxy->>Jdk/CGLIB: 调用业务方法
    Jdk/CGLIB->>ChainFactory: getInterceptorsAndDynamicInterceptionAdvice(config, method, targetClass)
    loop 遍历所有Advisor
        ChainFactory->>AllAdvisors: 获取单个Advisor
        alt PointcutAdvisor
            ChainFactory->>AllAdvisors: getPointcut().getMethodMatcher().matches(method, targetClass)
        else 非PointcutAdvisor
            Note over ChainFactory: 始终匹配
        end
        alt 匹配成功
            ChainFactory->>Registry: 将Advice适配为MethodInterceptor
            Registry-->>ChainFactory: MethodInterceptor实例
        end
    end
    ChainFactory-->>Jdk/CGLIB: 返回有序的MethodInterceptor链

主旨概括:此序列图描述了从一个方法调用触发的拦截器链构建完整过程。 分解:代理入口持有 AdvisedSupport 配置,DefaultAdvisorChainFactory 遍历配置中所有 Advisor,利用 MethodMatcher 精确筛选出匹配当前方法的 Advisor,然后将其 Advice 通过适配器注册表转换为统一的 MethodInterceptor原理:构建过程兼顾静态匹配(类/方法名)和动态匹配(运行时参数),返回的链是排好序的。排序规则在 AspectJAwareAdvisorAutoProxyCreator 中实现,按照 @OrderOrdered 接口升序排列。 工程结论:理解构建过程对于诊断“为何某个通知未被应用”至关重要。可能原因包括切点表达式写错、Advisor 未注册、或是被 shouldSkip 过滤。

4.2 核心源码:DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice

@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
        Advised config, Method method, @Nullable Class<?> targetClass) {
    List<Object> interceptorList = new ArrayList<>(config.getAdvisors().length);
    Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
    for (Advisor advisor : config.getAdvisors()) {
        if (advisor instanceof PointcutAdvisor) {
            PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
            if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
                MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
                if (MethodMatchers.matches(mm, method, actualClass, false)) {
                    // 适配为 MethodInterceptor
                    MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
                    if (mm.isRuntime()) {
                        // 动态切入点,需要创建 InterceptorAndDynamicMethodMatcher
                        for (MethodInterceptor interceptor : interceptors) {
                            interceptorList.add(new InterceptorAndDynamicMethodMatcher(
                                    interceptor, mm));
                        }
                    } else {
                        interceptorList.addAll(Arrays.asList(interceptors));
                    }
                }
            }
        } else {
            // 非 PointcutAdvisor,始终匹配
            MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
            interceptorList.addAll(Arrays.asList(interceptors));
        }
    }
    return interceptorList;
}

解读

  • 匹配过滤:对于 PointcutAdvisor,先进行 ClassFilter 匹配,再获取 MethodMatcher 并调用其 matches 方法。若不匹配则直接忽略此 Advisor。
  • 动态切入点:若 MethodMatcher.isRuntime() 返回 true(例如切点表达式包含 @targetargs 等运行时变量),则需要将 MethodInterceptorMethodMatcher 打包成 InterceptorAndDynamicMethodMatcher,以便执行时再次检查。
  • 适配:通过 AdvisorAdapterRegistry.getInterceptors(advisor) 将 Advisor 内部的 Advice 转换为一个 MethodInterceptor 数组。内置适配器支持 MethodBeforeAdviceAfterReturningAdviceThrowsAdvice 以及直接实现 MethodInterceptor 的 Advice。
  • 最终链:返回的 List<Object> 是一个已经排好序的 拦截器链。排序不是在链构建时进行,而是提前在容器启动时对 Advisor 列表进行了全局排序。Spring 在 AspectJAwareAdvisorAutoProxyCreator 中通过 sortAdvisors 方法,使用 AnnotationAwareOrderComparator 对所有 Advisor 进行排序,该比较器会考虑 @OrderOrdered 接口和 @Priority。因此,拦截器链的顺序在构建前即已确定。

五、拦截器链的执行与通知顺序

拦截器链构建完毕后,代理调用入口会将其交给 ReflectiveMethodInvocation,通过递归调用 proceed() 驱动整个链的运转。这正是 Spring AOP 执行机制的精髓。

5.1 执行入口

我们分别看 JDK 和 CGLIB 代理的入口。

JDK 代理:JdkDynamicAopProxy.invoke

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // ... 处理 equals/hashCode/advised 接口等特殊方法
    // 获取目标对象
    TargetSource targetSource = this.advised.targetSource;
    Object target = targetSource.getTarget();
    // 获取当前方法的拦截器链
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    if (chain.isEmpty()) {
        // 无拦截器链,直接反射调用目标方法
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
    } else {
        // 构建 ReflectiveMethodInvocation 并执行 proceed()
        MethodInvocation invocation =
                new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
        retVal = invocation.proceed();
    }
    // ...
    return retVal;
}

CGLIB 代理:DynamicAdvisedInterceptor.intercept 内部逻辑与 JDK 入口几乎一致,同样通过 AdvisedSupport 获取拦截器链,并创建 CglibMethodInvocation(继承自 ReflectiveMethodInvocation)调用 proceed()

5.2 ReflectiveMethodInvocation.proceed() 递归执行

public class ReflectiveMethodInvocation implements ProxyMethodInvocation {
    protected final List<?> interceptorsAndDynamicMethodMatchers;
    private int currentInterceptorIndex = -1;

    @Override
    public Object proceed() throws Throwable {
        // 当拦截器链遍历完毕,调用目标方法
        if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
            return invokeJoinpoint();
        }
        Object interceptorOrInterceptionAdvice =
                this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
        // 处理动态切入点
        if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
            InterceptorAndDynamicMethodMatcher dm =
                    (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
            if (dm.matcher.matches(this.method, this.targetClass, this.arguments)) {
                return dm.interceptor.invoke(this);
            } else {
                // 动态匹配失败,跳过此拦截器,继续执行下一个
                return proceed();
            }
        } else {
            return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
        }
    }

    protected Object invokeJoinpoint() throws Throwable {
        return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
    }
}

解读

  • 递归驱动proceed() 通过 currentInterceptorIndex 索引依次取出拦截器链中的 MethodInterceptor,并调用其 invoke(this)。每个拦截器的 invoke 方法内部在完成自己的前置处理后,通常会回调 MethodInvocation.proceed(),从而再次进入同一个 ReflectiveMethodInvocation 对象的 proceed(),索引递增,形成递归。
  • 责任链:当所有拦截器都执行完前置增强后,最终 proceed() 检测到索引到达末尾,调用 invokeJoinpoint() 反射执行目标方法。目标方法返回后,递归逐层返回,拦截器的后置处理得以在返回路径上执行。这完美实现了“同心圆”增强模型。
  • 动态匹配:若节点为 InterceptorAndDynamicMethodMatcher,则在调用前实时匹配;若匹配失败,则直接 proceed() 继续下一节点,该拦截器被完全跳过。

5.3 拦截器链执行序列图

sequenceDiagram
    participant Caller as 调用方
    participant Proxy as 代理对象
    participant Invocation as ReflectiveMethodInvocation
    participant AroundInt as @Around拦截器
    participant BeforeInt as @Before拦截器
    participant Target as 目标方法
    participant AfterRetInt as @AfterReturning拦截器
    participant AfterInt as @After拦截器
    Caller->>Proxy: 调用方法
    Proxy->>Invocation: 构建并调用proceed()
    Invocation->>AroundInt: invoke(mi) [前置]
    AroundInt->>Invocation: mi.proceed()
    Invocation->>BeforeInt: invoke(mi)
    BeforeInt->>Invocation: mi.proceed() (内部执行@Before通知后)
    Invocation->>Target: invokeJoinpoint()
    Target-->>Invocation: 返回值/异常
    Invocation-->>BeforeInt: 返回
    BeforeInt-->>Invocation: 返回
    Invocation->>AfterRetInt: invoke(mi) (proceed返回后执行通知)
    AfterRetInt-->>Invocation: 返回
    Invocation->>AfterInt: invoke(mi) (类似finally)
    AfterInt-->>Invocation: 返回
    Invocation-->>AroundInt: 返回
    AroundInt->>AroundInt: [后置] 执行@Around后置逻辑
    AroundInt-->>Invocation: 返回最终结果
    Invocation-->>Proxy: 返回结果
    Proxy-->>Caller: 返回

主旨概括:本序列图精确展示了包含多种通知类型时,proceed() 递归调用形成的调用时序。 分解@Around 拦截器先获得控制权,其前置部分执行后调用 proceed(),引发下一个拦截器 @Before 的执行;@Before 执行通知后再次调用 proceed(),直至目标方法被调用。在返回阶段,@AfterReturning@After 拦截器分别在 proceed() 返回后执行(通过回调),最后 @Around 拦截器执行其后置部分。 原理:每个拦截器就是一个栈帧,递归调用将它们压入调用栈,目标方法执行完毕后逐层弹出,后置增强自然呈相反顺序执行。 工程结论:这种递归责任链是 Spring AOP 的核心执行模型,深刻理解它有助于准确把握通知顺序和多切面协作行为。

5.4 五种通知类型的执行顺序规则

基于上述递归模型,五种通知的严格顺序如下(正常返回场景):

  1. @Around 前置:最先执行,因为它包裹在最外层。
  2. @Before:在 @Around 前置之后、目标方法之前执行。
  3. 目标方法:执行实际业务逻辑。
  4. @AfterReturning:在目标方法成功返回后执行。
  5. @After:在 @AfterReturning 之后执行,类似于 finally 块。
  6. @Around 后置:最后执行,包裹所有上述步骤。

异常场景下,@AfterThrowing 替代 @AfterReturning,但 @After 依然会执行(在 finally 语义下),@Around 后置可视情况处理异常。

多个同类型通知的排序

  • 对于 @Before 通知,多个按照 Order 值升序执行(值越小越先执行)。
  • 对于 @After 通知,多个按照 Order 值降序执行(值越小越后执行)。
  • 对于 @AfterReturning@AfterThrowing,同样遵循与 @After 相同的降序规则。

这是责任链“先入后出”的自然结果:Order 低的拦截器先入栈,后出栈;@After 系列通知是在栈弹出时执行,因此顺序反转。理解这一点对于多切面协同至关重要。

六、AOP 的设计权衡与工程边界

  • 代理式 vs 织入式:Spring AOP 的代理模式简洁,与容器深度集成,但无法拦截 self-invocation 和私有方法。AspectJ 功能强大但需要额外编译步骤或类加载器配置。大多数企业级应用中,Spring AOP 已足够。
  • JDK 动态代理 vs CGLIB:JDK 代理更轻量,但要求目标类实现接口;CGLIB 不需要接口,但可能遇到 final 方法限制。Spring Boot 2.x 默认 CGLIB 以降低使用门槛。
  • 性能考量:代理对象创建是一次性启动开销,每个方法调用需遍历拦截器链,有微量性能损耗,但 Spring 内部通过缓存拦截器链(AdvisedSupport 中有缓存机制)大幅减少了构建成本。
  • 与声明式事务/异步的关系@Transactional@Async 本质上都是通过 AOP 实现的。当它们共存于同一个 Bean 时,执行顺序由各自 Advisor 的 Order 决定。例如,@AsyncAsyncAnnotationAdvisor Order 通常较低,以保证事务切面在异步执行前开启。这部分知识将在后续事务篇章详细深入。

七、生产事故排查专题

案例1:@PostConstruct 中调用 @Async 方法不生效

现象:在 Bean 的 @PostConstruct 方法内调用标注了 @Async 的方法,期望异步执行,但日志显示调用线程未变化,方法同步执行完毕。

排查:在 @PostConstruct 方法内打印 this.getClass().getName(),发现类名并非代理类名,而是原始实现类,说明此时 Bean 还未被 AOP 代理。

根因分析:Bean 的生命周期顺序是:构造器 → 注入依赖 → @PostConstructpostProcessAfterInitialization(创建 AOP 代理)。当 @PostConstruct 调用时,代理对象尚未创建,this 仍是原始对象,@Async 的拦截器自然无法生效。源码角度,AbstractAutoProxyCreator.postProcessAfterInitialization@PostConstruct 回调之后才触发。

解决方案

  • 将需要异步执行的逻辑移至 ApplicationRunner 或监听 ContextRefreshedEvent 事件,此时代理已就绪。
  • 或者通过 @Lazy 注入自身代理,再在 @PostConstruct 中通过代理调用,但需注意循环依赖问题。

最佳实践:永远不要在初始化回调(@PostConstructInitializingBean.afterPropertiesSet)中依赖 AOP 增强能力,因为此时代理尚未完成。

案例2:this 调用导致 @Transactional 失效

现象:Service 类方法 A 内部通过 this.methodB() 调用另一个带有 @Transactional 注解的方法 B,B 方法内抛出 RuntimeException 但事务未回滚。

排查:检查数据库操作,发现 B 的修改已写入。在 B 处加入断点或日志,发现并未打印事务拦截器的日志。通过 AopUtils.isAopProxy(this) 检测结果为 false,确认 this 并非代理。

根因分析:Spring AOP 是基于代理的,只有通过代理对象执行的方法才会触发切面。this 是目标对象自身的引用,调用直接进入目标实例的 methodB,绕过了代理层,事务拦截器根本未插入。

解决方案

  • 通过 AopContext.currentProxy() 获取当前线程的代理对象,转为接口调用:((MyService) AopContext.currentProxy()).methodB(),并确保 exposeProxy = true(Spring Boot 中 spring.aop.proxy-target-class=true 并可通过 @EnableAspectJAutoProxy(exposeProxy = true) 开启)。
  • 将 methodB 拆分到另一个 Bean 中,通过注入代理对象调用。
  • 如果必须自调用,可通过构造器注入自身代理(注意循环依赖风险)。

最佳实践:避免 self-invocation,核心流程设计时确保外部调用入口在代理上;或将自调用方法重构为独立 Bean。

案例3:两个 @Around 通知 order 冲突导致逻辑异常

现象:项目中有两个切面都使用 @Around 匹配同一方法,日志显示某个切面的前置逻辑在另一个切面的后置逻辑之后执行,导致业务流程顺序混乱。

排查:分别查看两个切面类的注解 @Order 值,发现一个设置为值 1,另一个也为 1,或者未设置,默认均为最低优先级。由于 Order 相同,排序可能依赖类名或内部注册顺序,非确定性,导致包裹顺序与预期相反。

根因分析:Spring 对 Advisor 排序时,若 Order 相同,则比较取决于 DefaultListableBeanFactory 中的注册顺序。@Around 本质是 MethodInterceptor,低 Order 的拦截器先包裹,后执行后置部分。开发者误以为两个切面独立顺序,但实则是嵌套关系。

解决方案:明确指定 @Order 值,如 @Order(100)@Order(200),低值先执行前置、后执行后置。结合业务需求调整。

最佳实践:多切面共存必须显式声明 Order,不得依赖默认顺序。编写单元测试验证包裹顺序。

案例4:CGLIB 代理下 final 方法无法增强

现象:一个 Service Bean 的某个方法加了 @Async,但方法执行仍同步。观察 Bean 类名显示为 ...$$EnhancerBySpringCGLIB...,说明确实被 CGLIB 代理,但异步未生效。

排查:检查异步方法声明,发现方法被 final 修饰。在 CglibAopProxy.getProxy 逻辑中,Enhancer 创建子类时会忽略 final 方法,生成的子类不会重写该方法。因此调用该方法直接执行原始类逻辑,不会触发 DynamicAdvisedInterceptor.intercept

根因分析:CGLIB 是基于继承的子类代理,final 方法无法被重写,因此无法织入任何增强逻辑。同理,final 类完全无法创建 CGLIB 代理。

解决方案

  • 移除方法的 final 修饰。
  • 或者将方法逻辑提取到接口,并配置使用 JDK 动态代理(需要目标类实现接口)。
  • 如果必须保留 final 且需要增强,考虑 AspectJ LTW。

最佳实践:需要被 Spring AOP 增强的方法不能声明为 final,避免将业务 Bean 设计为 final 类。

案例5(额外):静态切点导致大量 Bean 被误代理

现象:应用启动时间明显变长,使用 Spring Boot Actuator 发现大量 Bean 类型为 CGLIB 代理,远超预期。

排查:检查日志中 Creating CGLIB proxy 信息,发现很多非目标 Bean 也被代理。追踪切点表达式,发现一个切面切点写为 execution(* com.example..*.*(..)),覆盖了整个包下所有类和方法。

根因分析AbstractAutoProxyCreator.wrapIfNecessary 中,该宽泛的切点匹配了几乎所有 Bean 的类和方法,导致每个 Bean 都被判断需要增强并创建代理。即使某些 Bean 方法上无实际通知逻辑,Spring 也会创建代理对象并为其分配空的拦截器链,造成内存和启动压力。

解决方案:将切点表达式精确到具体服务层或特定包,如 execution(* com.example.service..*.*(..)),并在可能的地方添加 && !@annotation(NoLog) 排除标记。

最佳实践:切点声明应尽可能精确,在 @Pointcut 上做好注释,定期审查 Advisor 匹配情况。

八、面试高频专题

(本模块与正文严格分离,专为面试场景设计)

Q1:什么是 AOP?Spring AOP 是如何实现的? 标准回答:AOP(面向切面编程)将横切关注点模块化。Spring AOP 基于动态代理在运行时为目标对象创建代理对象,从而织入增强逻辑。它使用 BeanPostProcessorAbstractAutoProxyCreator)在 Bean 初始化后检查是否需要增强,若需要则用 JDK 或 CGLIB 生成代理。 追问

  • 解释 Spring AOP 为何不能拦截内部调用?
    答:因为代理模式只截获外部对代理的调用,this 是原始对象的引用,绕过了代理。
  • JDK 动态代理和 CGLIB 的主要差异?
    答:JDK 基于接口,CGLIB 基于继承;CGLIB 不能增强 final 方法。
  • 代理是在生命周期的哪个步骤创建的?
    答:postProcessAfterInitialization,即初始化后。 加分回答:可深入 wrapIfNecessary 的短路逻辑,或说明 Spring Boot 为何默认 CGLIB。

Q2:Spring AOP 和 AspectJ 的区别?各自适用于什么场景? 标准回答:Spring AOP 是运行时动态代理,只支持方法级连接点,不能拦截内部调用;AspectJ 是编译期/类加载期织入,功能强大,可对字段、构造器等切点,但需特殊编译器或 LTW。Spring AOP 适合大多数企业级场景,AspectJ 适合需要完全控制切面或无法使用代理的场景。 追问

  • 什么情况下必须用 AspectJ 而不是 Spring AOP?
    答:需要增强 final 类、私有方法、内部调用,或非 Spring 管理的对象。
  • Spring 如何集成 AspectJ?
    答:通过 LTW(Load Time Weaving)或 @EnableLoadTimeWeaving。
  • Spring AOP 的 Advisor 和 AspectJ 的 Aspect 如何对应?
    答:@Aspect 类被解析为多个 AspectJPointcutAdvisor加分回答:解释 Spring AOP 借用 AspectJ 注解语法但底层仍是代理的“偷梁换柱”设计。

Q3:JDK 动态代理和 CGLIB 代理的区别?Spring 如何选择? 标准回答:JDK 动态代理基于接口,生成 com.sun.proxy.$Proxy 类,要求目标对象实现至少一个接口;CGLIB 通过继承目标类生成子类代理,不能代理 final 方法/类。Spring 在 DefaultAopProxyFactory.createAopProxy() 中根据 proxyTargetClassisOptimize() 以及目标类是否实现了接口来决定。 追问

  • 如何强制使用 CGLIB?
    答:设置 proxyTargetClass=true(Spring Boot 2.x 默认如此)。
  • CGLIB 代理的对象能用 instanceof 判断实现接口吗?
    答:可以,因为生成的子类也实现了目标类所实现的接口。
  • 为什么构造函数调用不会被代理拦截?
    答:代理对象调用构造器后,容器才可能返回代理;初始化拦截没意义。 加分回答:源码级展示 DefaultAopProxyFactory 的 if-else 分支,并说明 JDK 代理性能通常更好。

Q4:为什么 Spring Boot 2.x 默认使用 CGLIB 代理? 标准回答:避免强制接口编程,减少因注入具体类而导致的 NoSuchBeanDefinitionException;统一代理模式,减少配置,符合开箱即用哲学。 追问

  • Spring Boot 1.x 默认是 JDK 代理?
    答:是的,那时需要手动设置 spring.aop.proxy-target-class=true
  • 使用 CGLIB 有什么潜在问题?
    答:final 方法失效、启动时生成类增加开销、方法数过多可能触及 CGLIB 限制。
  • 如果目标类没有默认构造器,CGLIB 能代理吗?
    答:可以,Spring CGLIB 使用 Objenesis 绕过构造器。 加分回答:结合 AopAutoConfiguration 自动配置源码,说明配置属性的默认值演变。

Q5:描述一个 Bean 被 AOP 代理的完整流程,从 Bean 初始化到代理生成。 标准回答:Bean 实例化 → 属性填充 → initializeBean(包括 @PostConstructInitializingBean.afterPropertiesSetinit-method) → postProcessAfterInitializationAbstractAutoProxyCreator.wrapIfNecessary → 获取匹配 Advisor → 决策代理类型 → ProxyFactory/DefaultAopProxyFactory → JDK 或 CGLIB 生成代理对象 → 返回代理并放入容器。 追问

  • 如果在 @PostConstruct 里调用另一个加了 @Async 的方法会怎样?
    答:异步失效,因为代理尚未创建。
  • 什么是提前代理引用?
    答:处理循环依赖时可能使用 getEarlyBeanReference 创建提前代理。
  • wrapIfNecessary 里的 shouldSkip 什么时候用?
    答:可以通过 extends AbstractAutoProxyCreator 实现自定义跳过逻辑。 加分回答:展示 postProcessAfterInitializationearlyProxyReferences 的处理,防止重复代理。

Q6:什么是 Advisor?它与 Aspect 和 Advice 的关系是什么? 标准回答:Advisor 是 Spring AOP 中的一个概念,将 Advice 和 Pointcut 组合为一个单元。一个 @Aspect 类可解析出多个 PointcutAdvisor(如一个 @Before 通知+切点构成一个 Advisor)。Advice 是增强行为,Aspect 是 Advisor 的集合。 追问

  • 为什么 Spring 要引入 Advisor?
    答:为了分离匹配逻辑(Pointcut)与增强逻辑(Advice),并复合它们,便于统一管理和排序。
  • IntroductionAdvisor 有什么特殊?
    答:它用于引介增强,可以为目标对象添加新接口实现。
  • 如何获取容器中所有的 Advisor?
    答:通过 BeanFactoryAdvisorRetrievalHelperAdvisorRetrievalHelper加分回答:说明 AspectJPointcutAdvisor 的 Pointcut 来自 AspectJExpressionPointcut,Advice 是 MethodBeforeAdvice 等。

Q7:Spring AOP 的拦截器链是如何构建的?如何排序? 标准回答:在方法调用时,DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice 遍历所有 Advisor,通过 MethodMatcher.matches() 匹配当前方法,然后将 Advice 适配为 MethodInterceptor 形成链。Advisor 的顺序在容器启动时由 AnnotationAwareOrderComparator 排序,优先 @Order,其次 Ordered 接口。 追问

  • 静态切入点和动态切入点的区别?
    答:静态在构建链时匹配一次,动态需在每次调用时检查参数。
  • 如何实现一个自定义的 Advisor?
    答:实现 PointcutAdvisor 接口并提供自己的切点和通知。
  • 拦截器链有缓存吗?
    答:AdvisedSupport 中有 methodCache,缓存方法->链映射。 加分回答:分析 getInterceptorsAndDynamicInterceptionAdvicemm.isRuntime() 的分支处理。

Q8:五种通知类型的执行顺序是什么?多个同类型通知又是如何排序的? 标准回答:@Around前置 → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around后置。多个 @Before 按 order 升序,多个 @After 按 order 降序,因为责任链递归调用“先入后出”。 追问

  • 为什么 @After 是降序?
    答:因为 @After 拦截器在 proceed 返回后执行,栈后进先出。
  • 如果 @AfterThrowing 捕获了异常,@After 还会执行吗?
    答:会,因为 @After 通常实现为 try-finally 结构。
  • 如何在同一个切面内定义多个通知并控制顺序?
    答:通过同一类中方法顺序无关,顺序由通知类型本身决定;多切面通过 Order 控制。 加分回答:画出递归调用栈的变化图,解释“同心圆”模型。

Q9:ReflectiveMethodInvocation.proceed() 是如何工作的?它用了什么设计模式? 标准回答proceed() 维护 currentInterceptorIndex 索引,每次取出一个拦截器调用其 invoke(this)。拦截器内部通常回调 proceed() 驱动下一个拦截器,直至链尾执行目标方法。这是典型的 责任链模式,通过递归实现。 追问

  • 如果链中某个拦截器不调用 proceed() 会发生什么?
    答:后续拦截器和目标方法都不会执行,方法直接返回。
  • 如何实现环绕通知的后置逻辑?
    答:在 @Around 拦截器中,try { Object result = mi.proceed(); ... 后置 } catch ... finally
  • 为什么选择递归而非迭代循环?
    答:递归天然处理调用栈层叠,方便在 proceed 返回后执行后置增强。 加分回答:展示 proceed() 源码核心片段,并对比 JDK 代理的 invoke 空链直接反射调用的快速路径。

Q10:为什么在 @PostConstruct 中调用 @Async 方法不生效? 标准回答:因为 @PostConstruct 在 Bean 初始化阶段被调用,那时 AOP 代理尚未创建(postProcessAfterInitialization 在其后)。this 还是原始对象,没有增强能力。 追问

  • 怎么解决?
    答:将调用移到 ApplicationRunner,或使用 @Lazy 注入自身代理(注意循环依赖)。
  • 类似问题会发生在 InitializingBean.afterPropertiesSet 中吗?
    答:会,同样发生在初始化阶段。
  • 如果用 @Lazy 循环注入自己,创建代理时不会死锁吗?
    答:Spring 通过三级缓存处理循环依赖,可成功。 加分回答:结合生命周期阶梯图说明执行次序,并指出这是新手常见陷阱。

Q11:什么是 self-invocation 问题?如何解决? 标准回答:在类内部通过 this 调用自身标注了 AOP 注解的方法,导致注解失效。因为 this 是目标对象而非代理,绕过了拦截器链。解决方案包括:通过 AopContext.currentProxy() 获取代理;将方法拆分到不同 Bean 通过注入调用;或使用 AspectJ LTW。 追问

  • exposeProxy 有什么副作用?
    答:将代理暴露给线程局部变量,可能被滥用,且必须通过 currentProxy 手动调用。
  • 拆分 Bean 会改变事务边界吗?
    答:可能,要评估事务传播行为。
  • 能否用 @Resource 注入自己来解决?
    答:不推荐,容易产生循环依赖且代码异味。 加分回答:解释 AopContext 基于 ThreadLocal,并说明如何用 AopUtils.isAopProxy 检测调用对象。

Q12:CGLIB 代理有哪些限制?final 方法、final 类、private 方法能增强吗? 标准回答:CGLIB 通过继承生成子类代理,因此 final 类和 final 方法无法被代理;private 方法由于不可见,子类也不能重写,故不能增强。静态方法属于类级别,无代理意义。 追问

  • 如果必须增强 final 方法怎么办?
    答:使用 AspectJ 编译期或类加载期织入。
  • CGLIB 代理对象的 getClass() 返回什么?
    答:类似 com.example.MyService$$EnhancerBySpringCGLIB$$...
  • final 类会导致启动异常吗?
    答:通常是 IllegalArgumentException 或 AOP 警告并返回原对象。 加分回答:展示 Enhancer.create 过程中对 Modifier.isFinal 的判断。

Q13:@Transactional 和 @Async 同时放在一个方法上,它们的执行顺序是怎样的?由什么决定? 标准回答:执行顺序由它们对应 Advisor 的 Order 决定。通常 @Async 的 Advisor Order 更低(如 AsyncAnnotationAdvisor 默认 getOrder 为 Ordered.LOWEST_PRECEDENCE),@TransactionalBeanFactoryTransactionAttributeSourceAdvisor Order 可能为 Ordered.LOWEST_PRECEDENCE 或自定义。若两者 Order 相同,则由注册顺序决定。一般建议显式设置 Order 以保证先事务后异步,或根据需求调整。 追问

  • 如果异步在前事务在后会发生什么?
    答:异步在新线程中执行,事务可能因线程切换而失效(除非传播事务上下文)。
  • 如何自定义 Advisor 的顺序?
    答:实现 Ordered 接口或使用 @Order 在切面类上。
  • 默认配置下同时使用会怎样?
    答:@Transactional 可能包裹 @Async,导致事务跨线程问题。 加分回答:结合 AbstractAdvisorAutoProxyCreatorsortAdvisors 的源码,解释 Order 的作用。

Q14:如何自定义一个 AOP 切面?需要哪些步骤? 标准回答

  1. 定义切面类并标注 @Aspect@Component
  2. 声明切点:@Pointcut("execution(...)")
  3. 编写通知方法,使用 @Before@After 等注解关联切点。
  4. 确保 Spring 启用 AOP:@EnableAspectJAutoProxy 或 Spring Boot 自动配置。 追问
  • 切面类自身会被代理吗?
    答:如果切面类有匹配自身方法的切点,可能导致递归调用,需注意排除。
  • 如何在通知中获取方法参数和返回值?
    答:通过 JoinPoint 参数或 @AfterReturning(pointcut="...", returning="ret")
  • 通知方法抛出异常后,后置通知还会执行吗?
    答:@After 会执行,@AfterReturning 不会。 加分回答:指出 ReflectiveAspectJAdvisorFactory 负责解析 @Aspect 为 Advisor。

Q15:如果两个切面都匹配了同一个方法,如何控制它们的执行顺序? 标准回答:在切面类上使用 @Order 注解或实现 Ordered 接口指定顺序。值越小优先级越高。@Around 会按该顺序嵌套,@Before 升序执行,@After 降序执行。 追问

  • 如果不指定 Order,默认顺序是什么?
    答:默认 Ordered.LOWEST_PRECEDENCE,多个同优先级按类名哈希等不确定顺序。
  • 能否在 XML 配置中控制顺序?
    答:可以按 <aop:aspect> 声明顺序,但不推荐。
  • 子类切面和父类切面同时存在顺序如何?
    答:切面类本身也是 Bean,遵守 Bean 顺序,@Order 对类生效。 加分回答:源码 AnnotationAwareOrderComparator 排序逻辑简要说明。

Q16:拦截器链是每次方法调用都重新构建的吗?Spring 做了哪些性能优化? 标准回答:拦截器链构建是有成本的。Spring 在 AdvisedSupport 中缓存方法对应的拦截器链(Map<MethodCacheKey, List<Object>> methodCache)。后续对相同方法的调用直接返回缓存链,避免重复匹配和适配。同时,CGLIB 代理尽可能使用 FixedChainGenerator 或在某些条件下直接内联拦截器。 追问

  • 动态切入点能够缓存吗?
    答:会缓存 InterceptorAndDynamicMethodMatcher 对象,但每次调用仍需动态匹配。
  • 缓存失效的场景是什么?
    答:当 Advisors 动态变化时(如运行时添加 Advisor),methodCache 会被清空。
  • 性能损耗主要在哪些环节?
    答:反射调用、递归调用栈开销,但在现代 JVM 上可忽略。 加分回答:展示 AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice 的缓存逻辑,并提到 Spring 5.x 的优化。

Q17:什么是引介增强(Introduction)?它和普通通知有什么不同? 标准回答:引介增强允许动态地为目标对象添加新的接口实现。它通过 DelegatingIntroductionInterceptorDefaultIntroductionAdvisor 实现。区别于普通通知只是对现有方法增强,引介可以改变对象的类型契约。例如,让一个未实现 Auditable 接口的 Bean 实现该接口。 追问

  • 引介增强用哪种代理?
    答:JDK 代理或 CGLIB 都可以。
  • 引介增强作用在类级别还是实例级别?
    答:实例级别,但通常为所有匹配的 Bean 实例附加。
  • Spring AOP 如何实现引介?
    答:代理对象实现额外接口,方法调用转发到拦截器。 加分回答:提及 IntroductionInfo 接口和 AnnotationDrivenIntroductionAdvisor

Q18(系统设计题一):设计一个方法级别的调用链追踪系统,记录入参、出参和耗时,并支持按 traceId 串联跨服务调用。利用 AOP 拦截链与通知顺序设计,并讨论异步调用 traceId 丢失问题。 标准回答

  • 设计:定义一个 @Trace 注解,编写切面类 TraceAspect,使用 @Around 通知。在 @Around 前置部分从 MDC 或 ThreadLocal 获取 traceId,若无则生成并放入;记录方法开始时间和入参;调用 joinPoint.proceed();后置部分记录耗时和出参。利用 @Order 确保 Trace 切面优先级较高(如 Ordered.HIGHEST_PRECEDENCE + 100),使其包裹其他业务切面,以获取完整调用链。
  • 跨服务串联:在 Feign 或 Rest 调用时,通过请求拦截器将 traceId 放入 HTTP Header 透传。下游服务从 Header 获取并设置 MDC。
  • 异步问题@Async 会导致 MDC 上下文丢失,因为 MDC 是线程局部。解决方案:自定义 TaskDecoratorExecutor 包装,将父线程 MDC 拷贝到子线程。也可以使用 ThreadPoolTaskExecutor.setTaskDecorator 实现。 追问
  • 如果 traceId 要在多个切面间共享,如何避免线程安全问题?
    答:ThreadLocal 天然线程隔离,MDC 底层就是 ThreadLocal,安全。
  • 如何处理反应式编程的 context 传递?
    答:使用 Reactor 的 ContextHooks,将 MDC 写入 Reactor Context。
  • 如何使用 AOP 实现链路追踪的采样功能?
    答:可以在 @Around 前置检查采样标记,若不采样则快速执行 joinPoint.proceed() 跳过记录。 加分回答:可提及 Sleuth 的设计思想,并解释 TraceInterceptor 这种全局 Agent 模式。

Q19(系统设计题二):对数百个 Service Bean 中的一部分方法进行权限校验,根据方法参数(如资源 ID)和当前用户校验权限。设计基于 AOP + 自定义注解的权限框架,并讨论性能优化。 标准回答

  • 设计:定义 @PreAuthorize(expression) 注解(可类似 Spring Security 但更轻量)。实现切面 AuthorizationAspect,在 @Before@Around 中解析方法参数和用户信息。通过 @Pointcut("@annotation(preAuthorize)") 进行匹配。通知内部使用表达式解析器或直接调用权限服务。
  • 性能优化
    • 切点匹配:使用基于注解的切点比包扫描更精确,避免全量 Bean 代理。
    • 拦截器链缓存:Spring AOP 自身有机缓存,性能影响较小。
    • 权限表达式预编译:若使用 SpEL,可缓存 Expression 对象。
    • 批量权限查询:对需要批量获取资源权限的场景,可在通知内收集后再批量查询,减少 DB 交互。
    • 确保切面 Order 较高,让权限通知较早执行,尽早拒绝无权限请求。
  • 集成:通过 SecurityContextHolder 获取当前用户。 追问
  • 如何处理没有注解但需要权限的默认安全场景?
    答:可以设计一个全局切点匹配所有 Service 方法,但性能开销大,建议使用 BeanPostProcessor 或自定义 Advisor。
  • 如果权限校验失败,如何全局统一处理异常?
    答:在切面中抛出特定权限异常,再由全局异常处理器转换为 HTTP 403。
  • 如何进行权限缓存的更新?
    答:可使用 Redis 或本地缓存,在权限变更时失效相关缓存 Key。 加分回答:可探讨基于 AspectJ 的 @DeclareParents 为 Bean 引入权限检查接口。

Q20:为什么 Spring AOP 无法增强内部调用?有没有办法绕过?原理是什么? 标准回答:代理对象截获的是外部对代理对象的方法调用。内部调用通过 this 指针直接访问原始对象,自然跳过代理层。绕过方法:注入自身代理(通过 ApplicationContext.getBean@Resource 注入)、AopContext.currentProxy()、或使用 AspectJ LTW。原理都是确保调用通过代理对象进行。 追问

  • 注入自己会不会造成循环依赖?
    答:Spring 三级缓存可以解决 setter/field 注入的循环依赖,构造器注入则会失败。
  • exposeProxy 有何限制?
    答:必须是 proxy-target-class 或 JDK 代理,且暴露给 ThreadLocal 有一定维护成本。
  • 为什么 Spring 团队不默认支持内部调用拦截?
    答:为了设计简洁和性能,代理模式天然如此,字节码增强复杂且易出错。 加分回答:深入 JdkDynamicAopProxy.invokethis.advised.exposeProxy 的设置逻辑。

九、Demo 代码示例

以下示例均基于 JDK 8 + Spring 5.x,可在 Spring Boot 项目中运行。

9.1 五种通知完整演示(排序示例)

@Aspect
@Component
public class DemoAspect {

    @Pointcut("execution(* com.example.service.DemoService.doSomething(..))")
    private void anyDoSomething() {}

    @Around("anyDoSomething()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Around before");
        Object result = pjp.proceed();
        System.out.println("Around after");
        return result;
    }

    @Before("anyDoSomething()")
    public void before(JoinPoint joinPoint) {
        System.out.println("Before advice");
    }

    @AfterReturning(pointcut = "anyDoSomething()", returning = "retVal")
    public void afterReturning(JoinPoint joinPoint, Object retVal) {
        System.out.println("AfterReturning advice");
    }

    @AfterThrowing(pointcut = "anyDoSomething()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        System.out.println("AfterThrowing advice");
    }

    @After("anyDoSomething()")
    public void after(JoinPoint joinPoint) {
        System.out.println("After advice");
    }
}

正常执行输出顺序:

Around before
Before advice
// 目标方法
AfterReturning advice
After advice
Around after

9.2 self-invocation 问题复现与修复

@Service
public class SelfService {
    @Transactional
    public void methodA() {
        System.out.println("methodA");
        this.methodB(); // self-invocation
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        System.out.println("methodB");
    }
}

复现:调用 methodA(),methodB 不会开启新事务。 修复:注入自身代理或使用 AopContext

// 在配置类加 @EnableAspectJAutoProxy(exposeProxy = true)
public void methodA() {
    ((SelfService) AopContext.currentProxy()).methodB();
}

9.3 @PostConstruct 异步失效示例

@Service
public class AsyncService {
    @Async
    public void asyncMethod() {
        System.out.println(Thread.currentThread().getName());
    }

    @PostConstruct
    public void init() {
        asyncMethod(); // 打印的仍是主线程,不会异步
    }
}

修复:通过 ApplicationRunner 调用。

十、AOP 核心概念速查表

通知类型Spring Advice 接口拦截器适配执行时机多个通知 Order 方向
@BeforeMethodBeforeAdviceMethodBeforeAdviceInterceptor目标方法前升序
@AfterReturningAfterReturningAdviceAfterReturningAdviceInterceptor目标方法成功返回后降序
@AfterThrowingThrowsAdviceThrowsAdviceInterceptor目标方法抛异常后降序
@After(特殊) AfterAdvice 系列AspectJAfterAdvice + 适配类似 finally 后降序
@AroundMethodInterceptor 直接实现无适配包裹整个调用嵌套(低Order在外)

十一、延伸阅读

  1. 《Spring 揭秘》王福强 —— AOP 实现章节,结合源码深入剖析 Spring AOP 的代理创建与拦截链。
  2. Spring Framework 官方文档 - Aspect Oriented Programming with Spring:详细说明注解使用和配置。
  3. AspectJ in Action (Ramnivas Laddad) —— 深入了解 AspectJ 语法与织入机制。
  4. 博客系列:Spring AOP 源码分析之 AbstractAutoProxyCreator 与 ReflectiveMethodInvocation 解析。
  5. 《设计模式之 Proxy 模式与动态代理》—— 理解代理模式在 Spring 中的设计取舍。