Spring AOP 核心原理
候选人小张坐在字节跳动的面试间里,面试官翻到简历上"熟练使用 Spring AOP 做统一日志记录",开口问道:
"Spring AOP 的原理是什么?"
小张说:"用的是动态代理。"面试官点点头,继续追问:"那 Spring AOP 和 AspectJ 有什么区别?"
小张停顿了两秒,说:"AspectJ 是编译期的?"面试官没说话,又问:"CGLIB 代理的 fastClass 机制是什么?"
小张开始擦汗。
【面试官心理】 我问他 AOP 原理,其实不是想听"动态代理"四个字。我想知道的是:他能不能说出 Spring AOP 的织入时机、为什么默认选 CGLIB、@EnableAspectJAutoProxy 到底干了什么。能答出这些的,基本都是 P6 以上的候选人。
一、AOP 核心概念 🔴
1.1 五个基本术语
AOP(面向切面编程)的五个核心概念,很多候选人能背出来,但面试官真正想知道的是:你能不能用自己的话说清楚,以及它们在 Spring 里怎么落地。
【面试官心理】 这五个概念我能倒背如成,所以我会跳过名词解释直接追问:请告诉我 Spring AOP 的织入时机是什么时候?为什么?
1.2 ❌ 错误示范
候选人原话:"Spring AOP 用的是 JDK 动态代理,所以能对所有类生效。"
问题诊断:
- 混淆了 JDK 动态代理和 CGLIB 的适用范围
- 没搞清楚 Spring AOP 默认用哪种代理
- 以为 AOP 能拦截所有类
面试官内心 OS:这个候选人连 Spring AOP 默认用 CGLIB 都不知道,肯定没看过源码。
1.3 标准回答
P5 级别:
Spring AOP 的原理是动态代理。Spring 在运行时为目标对象创建代理对象,代理对象持有目标对象的引用,在方法调用的前后织入通知逻辑。如果目标类实现了接口,Spring 使用 JDK 动态代理;如果没有实现接口,使用 CGLIB 字节码生成代理。
P6 级别:
Spring AOP 使用了两种代理机制。默认情况下,Spring 优先选择 CGLIB(从 Spring 3.2 开始 CGLIB 被打包进 spring-core,不再需要单独依赖)。JDK 动态代理要求目标类必须实现接口,代理类与目标类实现同一接口,所以只能拦截接口中声明的方法。而 CGLIB 通过继承目标类生成子类,覆盖非 final 方法来实现代理,可以拦截所有方法,包括 private 方法(注意:final 方法和 static 方法不能被代理)。
P7 级别:
Spring AOP 本质上是一个 AOP 织入框架,它的核心是
AopProxy接口体系。JdkDynamicAopProxy实现 AopProxy,通过Proxy.newProxyInstance()创建 JDK 动态代理;CglibAopProxy通过Enhancer创建 CGLIB 代理,并在内部实现了 FastClass 机制——所谓 FastClass,就是为目标类和代理类各生成一个 FastClass,将方法调用从"反射调用"降维到"直接方法调用",避免在热点路径上大量使用 Method.invoke() 带来的性能开销。这个设计在 Spring 4.x 中被引入,用来弥合 AOP 带来的性能差距。
二、Spring AOP vs AspectJ 🟡
2.1 织入时机对比
这是面试官最爱的追问点。两个字之差,背后是编译期处理和运行期处理的根本区别。
2.2 为什么 Spring 还要提供 AspectJ 支持?
Spring AOP 功能有限,但 AspectJ 配置复杂。Spring 的解法是:用 AspectJ 的语法,但用 Spring AOP 的织入时机。
关键注解是 @EnableAspectJAutoProxy。它做了什么?
AspectJAutoProxyRegistrar 做的事很直接:往容器里注册了一个 AnnotationAwareAspectJAutoProxyCreator(BeanPostProcessor)。这个后置处理器在 Bean 初始化完成后,扫描所有 @Aspect 注解的切面,为匹配的目标 Bean 生成代理。
【面试官心理】
我问他 @EnableAspectJAutoProxy,其实是在试探他有没有动手看过这个注解的源码,以及知不知道 proxyTargetClass 和 exposeProxy 两个参数的真正用途。大部分候选人只知道"加这个注解才能用 AOP",说不出背后的 BeanPostProcessor 机制。
三、两种代理深度对比 🔴
3.1 JDK 动态代理原理
JDK 动态代理的核心是 Proxy.newProxyInstance(),它需要三个要素:
- ClassLoader:用来加载生成的代理类
- Interface[]:目标类实现的接口数组(代理类和目标类实现相同接口)
- InvocationHandler:方法调用的处理逻辑
关键限制:JDK 代理只能代理接口,无法代理类。如果 UserService 是一个没有实现任何接口的普通类,JDK 动态代理直接报错。
3.2 CGLIB 代理原理
CGLIB(Code Generation Library)的核心是 Enhancer 类,它通过继承目标类生成子类,覆盖所有非 final 方法来实现拦截。
3.3 FastClass 机制
这是 P6/P7 的分水岭问题。
在 CGLIB 早期版本中,方法调用走的是 method.invoke(target, args) —— 每次调用都要做方法访问检查(JVM 的 AccessibleObject.checkAccess()),开销不小。Spring 4.x 引入 FastClass 后,生成的 FastClass 不走反射,直接通过索引调用方法:
调用链从 反射调用(Method.invoke)变成 直接调用(FastClass 索引查找),在高频调用场景下性能提升显著。
3.4 代理的失效场景 ⚠️
90% 的候选人答不出这三个坑:
坑一:内部调用(self-invocation)
坑二:private 方法永远无法被代理
CGLIB 通过继承实现,private 方法在子类中不可见,所以任何 AOP 通知对 private 方法都无效。
坑三:构造器返回时 target 已经变了
Spring AOP 的代理在目标对象的方法调用前后织入逻辑,但如果目标对象内部直接 new 了一个对象替换自己,代理管不到。
解决内部调用失效,有三种方案:一是用 AspectJ 编译器(编译时织入,绕过代理);二是注入自身(@Autowired private OrderService self);三是开启 exposeProxy = true,通过 AopContext.currentProxy() 获取代理对象。
四、追问升级
第一层追问:
面试官:"Spring AOP 是在什么时候创建代理对象的?"
候选人:"Bean 初始化的时候。"
考察点:BeanPostProcessor 机制
第二层追问:
面试官:"BeanPostProcessor 和普通 Bean 的执行顺序是什么?"
候选人:... (很多人答不上来)
考察点:Spring Bean 生命周期理解深度
第三层追问:
面试官:"为什么 Spring 默认使用 CGLIB 而不是 JDK 动态代理?"
候选人:"因为 CGLIB 更快?"
考察点:Spring 3.x 版本演进、proxyTargetClass 参数的理解
第四层追问:
面试官:"你刚才说 private 方法不能被代理,那 Spring 事务的 @Transactional 注解在 private 方法上会怎么样?"
候选人:... (P7 分水岭)
考察点:Spring 事务基于 AOP,私有方法上的注解等同于无效
【面试官心理】 AOP 这道题我通常用来筛选三个层次:知道动态代理的是 60 分,知道 CGLIB 和 fastClass 的是 80 分,知道内部调用失效和 private 方法陷阱的,才是真正在项目里踩过坑的。
五、生产避坑
线上场景:某服务在高峰期大量接口超时,排查发现是 AOP 切面里做了同步的日志 DB 写入。
排查方法:
- 开启 Spring AOP debug 日志:
logging.level.org.springframework.aop=DEBUG - 使用 Arthas 的
trace命令:trace com.xxx.* *观察方法耗时分布 - 检查切面中的 IO 操作(DB、文件、网络)
正确做法:切面中的耗时操作应该走异步队列,不阻塞主流程。