Spring(三):AOP 原理与代理机制
Spring(三):AOP 原理与代理机制
导语:AOP 是 Spring 面试里"能背概念但讲不透底层"的典型代表。本篇从术语与通知类型出发,讲透 Spring AOP 的实现本质——JDK 动态代理与 CGLIB 的抉择、
ProxyFactory的组装过程、代理的调用链,再回答那个最容易被追问的问题:为什么 private / final / 自调用 / 构造器代理不了,最后对比 AspectJ 并给出@Around、多切面顺序的实践要点,共 12 题。
一、AOP 基础
1. 什么是 AOP?核心术语有哪些?
答: AOP(Aspect-Oriented Programming,面向切面编程)把横切关注点(日志、事务、权限、监控、缓存、重试)从业务逻辑中剥离出来,集中到"切面"里复用,避免在业务代码中到处散落重复逻辑。
| 术语 | 含义 |
|---|---|
| Aspect(切面) | 横切逻辑的模块化单元(通知 + 切点的组合,@Aspect 类) |
| Join Point(连接点) | 程序执行中可以插入切面的点(Spring AOP 中只有方法执行这一种) |
| Pointcut(切点) | 匹配连接点的表达式(execution(...)、@annotation(...) 等) |
| Advice(通知) | 在连接点上执行的增强逻辑(Before/After/Around 等) |
| Target(目标对象) | 被代理的原始业务对象 |
| Proxy(代理对象) | 织入通知后生成的增强对象 |
| Weaving(织入) | 把切面应用到目标对象的过程。Spring AOP 是运行期织入(动态代理) |
关键差异:Spring AOP 的连接点粒度只到"方法"(不支持字段、构造器、静态初始化块),这是它与 AspectJ 最本质的能力差距。
2. Spring 有哪几种通知(Advice)类型?语义分别是什么?
答:
| 通知 | 注解 | 执行时机 | 能否阻止目标方法执行 |
|---|---|---|---|
| 前置 | @Before | 目标方法之前 | ❌ 不能(只能抛异常阻断) |
| 后置返回 | @AfterReturning | 目标方法正常返回后 | ❌ 不能 |
| 后置异常 | @AfterThrowing | 目标方法抛异常后 | ❌ 不能(不能吞掉异常) |
| 后置最终 | @After | 目标方法之后无论正常还是异常(相当于 finally) | ❌ 不能 |
| 环绕 | @Around | 包裹目标方法,可自由控制 | ✅ 能(不调用 proceed() 就不执行) |
同一目标方法上的执行关系(一个切面内):
@Around(proceed 之前)
→ @Before
→ 目标方法执行
→ @AfterReturning / @AfterThrowing(二选一)
→ @After(finally,总执行)
@Around(proceed 之后)
@Around是功能最强的通知:可控制是否执行、可修改入参与返回值、可捕获并处理异常、可做重试/缓存/超时——@Transactional、@Cacheable、@Async底层都是环绕通知。
3. Spring AOP 和 AspectJ 有什么区别?
答:
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行期(基于动态代理) | 编译期(ajc 编译器)/ 类加载期(LTW)字节码织入 |
| 连接点 | 仅方法执行(且通常仅 public) | 方法、构造器、字段读写、静态初始化等全部 |
| 性能 | 调用有代理开销(切面多时更明显) | 织入后几乎无运行期开销(编译期已改字节码) |
| 依赖 | 只需 Spring 容器 | 需 AspectJ 编译器/weaver 或 -javaagent |
| 与 Spring 的关系 | 复用 AspectJ 的注解与切点表达式(@Aspect、@Pointcut、execution(...)),但底层是代理,不是 AspectJ 编译器 | 独立完整的 AOP 解决方案 |
选型结论:
- 95% 的业务增强(事务、日志、缓存、埋点)用 Spring AOP 足够,因为业务逻辑都在 Spring Bean 的 public 方法上。
- 需要拦截非 Spring Bean 的方法、构造器、字段访问,或对性能极其敏感(超多切面)时,才引入 AspectJ(LTW)。
易错点:很多人说"Spring AOP 就是 AspectJ"——错。Spring 只是借用了 AspectJ 的注解和切点语法(
spring-aop+aspectjweaver),实现机制完全不同。
二、代理机制与底层原理
4. Spring AOP 底层用了哪两种动态代理?有什么区别?
答:
| 对比项 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 原理 | 基于接口,运行期生成实现该接口的代理类(Proxy + InvocationHandler) | 基于继承,运行期用 ASM 生成目标类的子类(Enhancer + MethodInterceptor) |
| 前提 | 目标必须实现接口 | 目标类不能是 final、被代理方法不能是 final / private / static |
| 生成方式 | JDK 反射(ProxyGenerator) | ASM 生成字节码(Spring 5.2 起内置 repackage 的 CGLIB) |
| 调用链 | invoke() → 反射调用目标方法 | intercept() → MethodProxy.invokeSuper()(FastClass 索引,非反射) |
| 性能 | JDK 8+ 生成与调用性能已很好 | 代理类生成稍慢(首次更明显),调用通常更快 |
| 额外成本 | 需要有接口 | 无法代理 final 类/方法;对私有方法无效 |
// JDK:代理类实现同一接口
public Object invoke(Object proxy, Method method, Object[] args) { ... }
// CGLIB:代理类继承目标类并重写方法
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) {
return mp.invokeSuper(obj, args); // 调用父类(目标)实现
}5. Spring 如何决定使用 JDK 代理还是 CGLIB?
答: 决策在 DefaultAopProxyFactory#createAopProxy():
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) throw new AopConfigException(...);
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config); // 目标本身就是接口 → JDK
}
return new ObjenesisCglibAopProxy(config); // 其余 → CGLIB
} else {
return new JdkDynamicAopProxy(config);
}结论:
proxyTargetClass = true→ 优先 CGLIB;- 目标类没有实现任何接口 → CGLIB;
- 目标类本身是接口(如
@Bean返回接口类型且无实现类信息)→ 只能 JDK; - 其余(有接口且未强制)→ 默认 JDK。
Spring Boot 的差异:Boot 通过
AopAutoConfiguration默认把proxyTargetClass设为 true(spring.aop.proxy-target-class=true),因此 Boot 应用里绝大多数场景都是 CGLIB。这也带来一个常见坑:Bean 被 CGLIB 代理后,注入时不能按"原始类型"强转的问题、以及@Autowired按接口注入仍可正常工作——但若代码里按类类型强转且代理未实现该具体类,就会ClassCastException。
6. 两种代理的调用链有什么区别?
答:
JDK:
caller → $Proxy0.method()
→ JdkDynamicAopProxy.invoke(proxy, method, args)
→ 构造 MethodInvocation(ReflectiveMethodInvocation)
→ 责任链依次执行 Advisor(通知)
→ AopUtils.invokeJoinpointUsingReflection() ← 【反射】调用目标方法
CGLIB:
caller → Target$$EnhancerBySpringCGLIB.method()
→ DynamicAdvisedInterceptor.intercept(obj, method, args, methodProxy)
→ 构造 CglibMethodInvocation
→ 责任链依次执行 Advisor(通知)
→ methodProxy.invokeSuper(obj, args) ← 【FastClass 索引】调用父类方法要点:
- 两者的通知链模型是一致的:都是"责任链(
MethodInvocation)+ 环绕式递归调用"; - 差异只在最内层如何调用目标方法:JDK 用反射(
Method.invoke),CGLIB 用MethodProxy的 FastClass 索引调用(少一次反射开销); - 代理链只在 Bean 初始化时创建一次,之后是缓存复用(因此"代理创建慢"只在启动/首次)。
7. Spring AOP 的底层组件(Advisor / Advice / ProxyFactory)是如何协作的?
答: 这是"讲到底层"的加分题,可用一条线说清:
@Aspect 类
→ AnnotationAwareAspectJAutoProxyCreator(一个 BeanPostProcessor)
· 解析 @Aspect/@Pointcut/@Before… → 每个通知生成一个 Advisor(包含 Advice + Pointcut)
→ 在 Bean 初始化后(postProcessAfterInitialization)
· 用 Pointcut 判断该 Bean 是否要被增强(无匹配则直接返回原对象,零开销)
· 有匹配 → 通过 ProxyFactory(内部按 proxyTargetClass 选 JdkDynamicAopProxy / CglibAopProxy)创建代理
· 若 Bean 已实现多个接口/需多切面,Advisor 按 @Order 排序后组成责任链
→ 调用时代理进入 MethodInvocation,逐层执行通知,最终调用目标方法关键类对照:
| 组件 | 职责 |
|---|---|
AnnotationAwareAspectJAutoProxyCreator | 扫描 @Aspect、生成 Advisor、为 Bean 创建代理(BPP) |
Advisor | = Pointcut + Advice,即"在哪些点做什么增强" |
ProxyFactory / ProxyCreatorSupport | 组装代理(AOP 的"工厂") |
MethodInvocation(ReflectiveMethodInvocation) | 责任链载体,串起所有通知并最终调用目标方法 |
AopContext | 暴露当前代理(currentProxy()),用于解决自调用问题 |
加分的理解:AOP 的本质不是"代理",而是"责任链"——代理只是入口,真正的增强逻辑靠
MethodInvocation这层递归链式调用。
8. Spring 5.2 / 6 的代理实现有什么变化?
答:
- CGLIB 被 repackage 进 Spring(
org.springframework.cglib),不再依赖外部cglib-nodep,避免与用户引入的 CGLIB/ASM 版本冲突;同时基于 ASM 重写、使用 Objenesis 绕过构造器实例化代理对象,生成更快。 - Spring Boot 2.x 起 AOP 默认
proxyTargetClass = true,统一使用 CGLIB——所以"有接口用 JDK"这条老规则在 Boot 项目里通常已不成立。 - Spring 6 / Boot 3:迁移到 Jakarta EE 9+(
javax.*→jakarta.*)、支持 AOT/Native Image,代理相关的反射在 AOT 阶段需要元数据登记(@Reflective、RuntimeHints)。
面试提示:被问"Spring AOP 默认用哪种代理"时,标准答法是"Spring Framework 默认按有无接口决策,Spring Boot 默认 CGLIB"——区分这两个层次才显专业。
三、AOP 的边界与失效场景
9. 为什么 private、final、static 方法以及构造器无法被 Spring AOP 拦截?
答: 全部源于"基于代理"这一前提:
| 目标 | 为什么不行 |
|---|---|
private 方法 | 子类(CGLIB)无法重写父类的 private 方法;JDK 代理只能代理接口的 public 方法 |
final 方法 | 子类无法重写 final 方法;final 类则无法被继承(CGLIB 直接失败) |
static 方法 | 静态方法属于类,不属于实例,this 调用不经过代理对象 |
| 构造器 | 代理对象是在目标对象实例化之后才创建的,构造期还没有代理 |
| 非 Spring 管理的对象 | 没有经过容器 → 根本没有代理(自己 new 的对象不受 AOP 管辖) |
非 public 方法(针对 @Transactional/@Async) | Spring 的事务/异步属性解析(AbstractFallbackTransactionAttributeSource、AsyncAnnotationBeanPostProcessor)对非 public 方法直接返回 null,因此明确不生效(CGLIB 虽能代理 protected,但注解不会被解析) |
结论:AOP 只能增强"外部通过代理对象调用的、public 的、非 final 的实例方法"。
10. 为什么"类内部方法自调用"会导致 AOP 失效?怎么解决?
答: 因为自调用走的是 this,而不是容器注入的代理对象:
@Service
public class OrderService {
public void a() {
this.b(); // ← 这里 this 是"目标对象",不是代理 → b() 上的 @Transactional/@Cacheable 全部失效
}
@Transactional
public void b() { ... }
}外部调用:caller → 代理对象.a() → 通知链 → 目标对象.a() → this.b() (绕过代理,通知链不再进入)四种解决方案:
| 方案 | 做法 | 评价 |
|---|---|---|
| 拆分到另一个 Bean(推荐) | 把 b() 挪到独立 Service,注入后调用 | 最干净,符合单一职责,无框架耦合 |
AopContext.currentProxy() | 需开启 @EnableAspectJAutoProxy(exposeProxy = true),用 ((OrderService) AopContext.currentProxy()).b() | 可用,但代码与 Spring API 耦合 |
自注入(@Lazy 或 ApplicationContext) | 注入自己:@Autowired @Lazy private OrderService self; 然后 self.b() | 能解决,但存在循环依赖风险 |
改用编程式事务 / TransactionTemplate | 不依赖代理,手动控制事务边界 | 适合需要精细控制事务的场景 |
注意:
@Cacheable、@Async、@Retryable、@Transactional全部都受自调用影响,原因是同一个——都依赖代理拦截外部调用。
11. 使用 @Around 通知时要注意什么?
答:
- 必须声明
ProceedingJoinPoint参数,并显式调用proceed()——不调用则目标方法不执行(这是"沉默的错误"); - 必须返回
proceed()的返回值,否则调用方拿到null; proceed()可以带参数(proceed(Object[] args))来修改入参;- 异常处理要谨慎:可以捕获并改写异常,但要注意别破坏原有事务语义(如吞掉异常导致事务不回滚);
- 注意与
@Transactional的顺序:@Around在事务通知之外还是之内,会影响回滚与提交时机; - 别做重活:
@Around包裹每次调用,日志打印大对象、远程调用会显著增加 RT。
@Around("execution(* com.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
Object result = pjp.proceed(); // 必须调用并返回
return result;
} finally {
metrics.record(System.nanoTime() - start); // 统计耗时
}
}12. 多个切面的执行顺序如何控制?AOP 的性能如何考量?
答(执行顺序):
- 多个切面之间用
@Order(n)/Ordered接口控制顺序:@Order值越小,优先级越高、越靠外层(前置通知越先执行、后置通知越后执行); - 不指定
Order时顺序不确定(取决于注册顺序),生产上必须显式指定——典型的如"日志切面在外、事务切面在内",或"限流切面在最外层"; - 典型编排:
@Order(0)限流/监控 →@Order(10)日志/追踪 →@Order(20)事务 → 业务(事务应尽量靠内,避免包裹无关逻辑导致长事务)。
性能考量:
| 因素 | 影响 |
|---|---|
| 代理创建 | 一次性成本(CGLIB 需生成字节码),可通过 lite 模式/减少切面数缓解 |
| 每次调用的链式开销 | 每个 Advisor 一次方法调用开销,切面很多时明显(可用压测评估) |
| 点切匹配 | 无匹配的 Bean 不会创建代理(AbstractAutoProxyCreator 提前判断),所以"给所有 Bean 都写切点"不会让无关注入变慢 |
| 切面内逻辑 | 真正的大头——在切面里打日志/发请求/加锁才是性能杀手 |
实践建议:切点表达式尽量收窄(按包/注解匹配,而非
execution(* *(..))全量),切面内避免重逻辑,并用@Order固定顺序。
本章小结:Spring AOP 的完整因果链是「基于代理 → 只能拦 public 非 final 实例方法 → 自调用失效 → 事务/缓存/异步都可能失效」。把这条链讲通,比孤立地背"JDK vs CGLIB"更有说服力。
