概述
在《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
上图以七个核心模块串联了本文的认知路径,层次递进、边界清晰:
-
概述与AOP概念
从横切关注点出发,明确 Spring AOP 的代理式 AOP 定位。厘清 JoinPoint、Pointcut、Advice、Aspect、Advisor、Weaving 等核心术语及其关系,为后续源码分析奠定理论基础。 -
代理创建机制
深入 Bean 生命周期中postProcessAfterInitialization这一精确节点,剖析AbstractAutoProxyCreator如何通过wrapIfNecessary判断是否需要代理,再借助DefaultAopProxyFactory的决策树选择 JDK 动态代理或 CGLIB 代理。完整展现一个原始 Bean 如何“穿上代理壳”的全流程。 -
通知与拦截链构建
讲解@Before、@AfterReturning、@AfterThrowing、@After、@Around五种通知类型如何通过适配器模式转换为统一的MethodInterceptor。在此基础上,拆解DefaultAdvisorChainFactory如何通过切点匹配、排序和适配,将分散的 Advisor 编织成一条严格有序的拦截器链。 -
拦截链执行与通知顺序
以ReflectiveMethodInvocation.proceed()的递归调用为核心,揭示责任链模式如何通过“压栈-出栈”实现环绕增强效果。系统归纳五种通知类型的固定执行顺序,以及多个同类型通知时@Before升序、@After降序的内在原理。 -
设计权衡
从工程视角对比代理式与织入式 AOP 的取舍、JDK 代理与 CGLIB 的性能与限制,并简要讨论 Spring AOP 与声明式事务、异步等模块的协同关系,帮助读者建立完整的设计边界认知。 -
生产事故
独立模块,用至少四个典型线上事故(@PostConstruct 失效、self‑invocation 失效、Order 冲突、final 方法限制),严格遵循“现象→排查→根源→解决→最佳实践”五步法拆解,让原理直接落地为排错能力。 -
面试专题
严格与正文知识模块隔离,整理不少于 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 定义了一组连接点的过滤规则,通过
ClassFilter和MethodMatcher来确定哪些 JoinPoint 需要增强。 - Advice 是增强行为的抽象,Spring 将其分为
MethodBeforeAdvice、AfterReturningAdvice、ThrowsAdvice和MethodInterceptor。 - 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;随后过滤基础设施类(如Advice、Advisor自身)和自定义跳过逻辑(shouldSkip)。 - 匹配 Advisor:核心调用
getAdvicesAndAdvisorsForBean。该方法在AbstractAdvisorAutoProxyCreator中实现,会遍历容器中所有的Advisor(包括 @Aspect 解析出的 Advisor),通过PointcutAdvisor.getPointcut().getClassFilter()和getMethodMatcher()检查是否与当前 Bean 的类及方法匹配。若匹配,则收集此 Advisor。 - 决策:若存在任何一个匹配的 Advisor,则
specificInterceptors不为空,进入代理创建阶段;否则标记为无需代理并直接返回原始 Bean。
2.3 代理工厂的选择:DefaultAopProxyFactory.createAopProxy
当决定创建代理后,Spring 将任务委托给 AopProxyFactory。DefaultAopProxyFactory 根据配置的 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])));
}
}
解读:
- 决策树:
- 如果
config.isOptimize()为 true,或者config.isProxyTargetClass()为 true,或者目标类没有提供任何用户接口(只有SpringProxy标记接口),则倾向于使用 CGLIB。 - 然而,即使优先 CGLIB,如果
targetClass本身是接口或者已经是java.lang.reflect.Proxy,则必须回退到 JDK 动态代理。 - 如果上述条件都不满足,即目标类实现了用户接口且未强制
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。
- @Before →
AspectJMethodBeforeAdvice,适配为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(); } } - @AfterReturning →
AspectJAfterReturningAdvice,适配为AfterReturningAdviceInterceptor,其invoke在proceed()返回后执行通知逻辑。 - @AfterThrowing →
AspectJAfterThrowingAdvice,适配为ThrowsAdviceInterceptor,在proceed()抛出异常时执行。 - @After →
AspectJAfterAdvice,适配为AfterReturningAdviceInterceptor类似,但不论正常还是异常都会执行(本质通过 finally 机制)。 - @Around →
AspectJAroundAdvice,自身直接实现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 中实现,按照 @Order 或 Ordered 接口升序排列。
工程结论:理解构建过程对于诊断“为何某个通知未被应用”至关重要。可能原因包括切点表达式写错、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(例如切点表达式包含@target、args等运行时变量),则需要将MethodInterceptor与MethodMatcher打包成InterceptorAndDynamicMethodMatcher,以便执行时再次检查。 - 适配:通过
AdvisorAdapterRegistry.getInterceptors(advisor)将 Advisor 内部的 Advice 转换为一个MethodInterceptor数组。内置适配器支持MethodBeforeAdvice、AfterReturningAdvice、ThrowsAdvice以及直接实现MethodInterceptor的 Advice。 - 最终链:返回的
List<Object>是一个已经排好序的 拦截器链。排序不是在链构建时进行,而是提前在容器启动时对Advisor列表进行了全局排序。Spring 在AspectJAwareAdvisorAutoProxyCreator中通过sortAdvisors方法,使用AnnotationAwareOrderComparator对所有 Advisor 进行排序,该比较器会考虑@Order、Ordered接口和@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 五种通知类型的执行顺序规则
基于上述递归模型,五种通知的严格顺序如下(正常返回场景):
- @Around 前置:最先执行,因为它包裹在最外层。
- @Before:在 @Around 前置之后、目标方法之前执行。
- 目标方法:执行实际业务逻辑。
- @AfterReturning:在目标方法成功返回后执行。
- @After:在 @AfterReturning 之后执行,类似于 finally 块。
- @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 决定。例如,@Async的AsyncAnnotationAdvisorOrder 通常较低,以保证事务切面在异步执行前开启。这部分知识将在后续事务篇章详细深入。
七、生产事故排查专题
案例1:@PostConstruct 中调用 @Async 方法不生效
现象:在 Bean 的 @PostConstruct 方法内调用标注了 @Async 的方法,期望异步执行,但日志显示调用线程未变化,方法同步执行完毕。
排查:在 @PostConstruct 方法内打印 this.getClass().getName(),发现类名并非代理类名,而是原始实现类,说明此时 Bean 还未被 AOP 代理。
根因分析:Bean 的生命周期顺序是:构造器 → 注入依赖 → @PostConstruct → postProcessAfterInitialization(创建 AOP 代理)。当 @PostConstruct 调用时,代理对象尚未创建,this 仍是原始对象,@Async 的拦截器自然无法生效。源码角度,AbstractAutoProxyCreator.postProcessAfterInitialization 在 @PostConstruct 回调之后才触发。
解决方案:
- 将需要异步执行的逻辑移至
ApplicationRunner或监听ContextRefreshedEvent事件,此时代理已就绪。 - 或者通过
@Lazy注入自身代理,再在@PostConstruct中通过代理调用,但需注意循环依赖问题。
最佳实践:永远不要在初始化回调(@PostConstruct、InitializingBean.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 基于动态代理在运行时为目标对象创建代理对象,从而织入增强逻辑。它使用 BeanPostProcessor(AbstractAutoProxyCreator)在 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() 中根据 proxyTargetClass、isOptimize() 以及目标类是否实现了接口来决定。
追问:
- 如何强制使用 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(包括 @PostConstruct、InitializingBean.afterPropertiesSet、init-method) → postProcessAfterInitialization → AbstractAutoProxyCreator.wrapIfNecessary → 获取匹配 Advisor → 决策代理类型 → ProxyFactory/DefaultAopProxyFactory → JDK 或 CGLIB 生成代理对象 → 返回代理并放入容器。
追问:
- 如果在
@PostConstruct里调用另一个加了@Async的方法会怎样?
答:异步失效,因为代理尚未创建。 - 什么是提前代理引用?
答:处理循环依赖时可能使用getEarlyBeanReference创建提前代理。 wrapIfNecessary里的shouldSkip什么时候用?
答:可以通过extends AbstractAutoProxyCreator实现自定义跳过逻辑。 加分回答:展示postProcessAfterInitialization中earlyProxyReferences的处理,防止重复代理。
Q6:什么是 Advisor?它与 Aspect 和 Advice 的关系是什么?
标准回答:Advisor 是 Spring AOP 中的一个概念,将 Advice 和 Pointcut 组合为一个单元。一个 @Aspect 类可解析出多个 PointcutAdvisor(如一个 @Before 通知+切点构成一个 Advisor)。Advice 是增强行为,Aspect 是 Advisor 的集合。
追问:
- 为什么 Spring 要引入 Advisor?
答:为了分离匹配逻辑(Pointcut)与增强逻辑(Advice),并复合它们,便于统一管理和排序。 IntroductionAdvisor有什么特殊?
答:它用于引介增强,可以为目标对象添加新接口实现。- 如何获取容器中所有的 Advisor?
答:通过BeanFactoryAdvisorRetrievalHelper或AdvisorRetrievalHelper。 加分回答:说明AspectJPointcutAdvisor的 Pointcut 来自AspectJExpressionPointcut,Advice 是MethodBeforeAdvice等。
Q7:Spring AOP 的拦截器链是如何构建的?如何排序?
标准回答:在方法调用时,DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice 遍历所有 Advisor,通过 MethodMatcher.matches() 匹配当前方法,然后将 Advice 适配为 MethodInterceptor 形成链。Advisor 的顺序在容器启动时由 AnnotationAwareOrderComparator 排序,优先 @Order,其次 Ordered 接口。
追问:
- 静态切入点和动态切入点的区别?
答:静态在构建链时匹配一次,动态需在每次调用时检查参数。 - 如何实现一个自定义的 Advisor?
答:实现PointcutAdvisor接口并提供自己的切点和通知。 - 拦截器链有缓存吗?
答:AdvisedSupport中有methodCache,缓存方法->链映射。 加分回答:分析getInterceptorsAndDynamicInterceptionAdvice中mm.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),@Transactional 的 BeanFactoryTransactionAttributeSourceAdvisor Order 可能为 Ordered.LOWEST_PRECEDENCE 或自定义。若两者 Order 相同,则由注册顺序决定。一般建议显式设置 Order 以保证先事务后异步,或根据需求调整。
追问:
- 如果异步在前事务在后会发生什么?
答:异步在新线程中执行,事务可能因线程切换而失效(除非传播事务上下文)。 - 如何自定义 Advisor 的顺序?
答:实现Ordered接口或使用@Order在切面类上。 - 默认配置下同时使用会怎样?
答:@Transactional可能包裹@Async,导致事务跨线程问题。 加分回答:结合AbstractAdvisorAutoProxyCreator中sortAdvisors的源码,解释 Order 的作用。
Q14:如何自定义一个 AOP 切面?需要哪些步骤? 标准回答:
- 定义切面类并标注
@Aspect和@Component。 - 声明切点:
@Pointcut("execution(...)")。 - 编写通知方法,使用
@Before、@After等注解关联切点。 - 确保 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)?它和普通通知有什么不同?
标准回答:引介增强允许动态地为目标对象添加新的接口实现。它通过 DelegatingIntroductionInterceptor 和 DefaultIntroductionAdvisor 实现。区别于普通通知只是对现有方法增强,引介可以改变对象的类型契约。例如,让一个未实现 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 是线程局部。解决方案:自定义TaskDecorator或Executor包装,将父线程 MDC 拷贝到子线程。也可以使用ThreadPoolTaskExecutor.setTaskDecorator实现。 追问: - 如果 traceId 要在多个切面间共享,如何避免线程安全问题?
答:ThreadLocal 天然线程隔离,MDC 底层就是 ThreadLocal,安全。 - 如何处理反应式编程的 context 传递?
答:使用 Reactor 的Context或Hooks,将 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.invoke中this.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 方向 |
|---|---|---|---|---|
| @Before | MethodBeforeAdvice | MethodBeforeAdviceInterceptor | 目标方法前 | 升序 |
| @AfterReturning | AfterReturningAdvice | AfterReturningAdviceInterceptor | 目标方法成功返回后 | 降序 |
| @AfterThrowing | ThrowsAdvice | ThrowsAdviceInterceptor | 目标方法抛异常后 | 降序 |
| @After | (特殊) AfterAdvice 系列 | AspectJAfterAdvice + 适配 | 类似 finally 后 | 降序 |
| @Around | MethodInterceptor 直接实现 | 无适配 | 包裹整个调用 | 嵌套(低Order在外) |
十一、延伸阅读
- 《Spring 揭秘》王福强 —— AOP 实现章节,结合源码深入剖析 Spring AOP 的代理创建与拦截链。
- Spring Framework 官方文档 - Aspect Oriented Programming with Spring:详细说明注解使用和配置。
- AspectJ in Action (Ramnivas Laddad) —— 深入了解 AspectJ 语法与织入机制。
- 博客系列:Spring AOP 源码分析之 AbstractAutoProxyCreator 与 ReflectiveMethodInvocation 解析。
- 《设计模式之 Proxy 模式与动态代理》—— 理解代理模式在 Spring 中的设计取舍。