Spring AOP 原理深度解析:从动态代理到切面织入(最新!Spring6与Spring5的差异)

Spring AOP 原理深度解析:从动态代理到切面织入(最新!Spring6与Spring5的差异)

摘要 :为什么同类方法调用 @Transactional 会失效?为什么 AOP 代理不了 static 方法?JDK 动态代理和 CGLIB 到底有什么区别?拦截器链又是怎样一步步把通知串起来的?这篇文章从源码级别逐一解答,附带可直接运行的单元测试。
温馨提示 :最新Spring6和Spring5这块AOP又有什么不同?SpringBoot3.x的默认代理策略是什么?文中皆有探讨,值得关注!
标签:Spring、AOP、动态代理、JDK代理、Cglib、切面、环绕通知、责任链


  • 一、从一个实际问题说起

  • 二、AOP 是什么?

  • 2.1 概念定义

  • 2.2 为什么需要 AOP

  • 三、AOP 核心术语

  • 3.1 术语图解

  • 3.2 术语详解

  • 四、五种通知类型

  • 4.1 通知类型执行顺序

  • Spring 5(Spring Boot 2.x)的执行顺序

  • Spring 6(Spring Boot 3.x)的执行顺序

  • 4.2 @Before 前置通知

  • 4.3 @AfterReturning 返回通知

  • 4.4 @AfterThrowing 异常通知

  • 4.5 @After 最终通知

  • 4.6 @Around 环绕通知(最强大)

  • 4.7 通知类型对比表

  • 五、切点表达式详解

  • 5.1 execution 表达式(最常用)

  • 5.2 within 表达式

  • 5.3 @annotation 表达式

  • 5.4 组合表达式

  • 六、AOP 底层原理(核心章节)

  • 6.1 两种代理方式(版本也有差异!)

  • 6.2 JDK 动态代理:完整源码分析

  • Proxy.newProxyInstance 内部做了什么

  • InvocationHandler.invoke 如何分发到目标方法

  • 6.3 CGLIB 代理:完整源码分析

  • Enhancer.create 如何生成子类

  • MethodInterceptor.intercept 的拦截流程

  • invokeSuper vs invoke 的区别

  • final 方法为什么不能被 CGLIB 代理

  • 6.4 拦截器链执行原理(责任链模式)

  • CglibMethodInvocation.proceed 的责任链模式

  • 多个通知如何组成拦截器链

  • 6.5 AnnotationAwareAspectJAutoProxyCreator 工作原理

  • 从 @EnableAspectJAutoProxy 到注册

  • postProcessAfterInitialization 中如何判断是否需要创建代理

  • 6.6 @EnableAspectJAutoProxy(exposeProxy=true) 底层实现

  • AopContext 的 ThreadLocal 机制

  • currentProxy() 如何获取当前代理

  • setCurrentProxy(oldProxy) 设计哲学

  • 类比理解:函数调用栈

  • 总结对比

  • 为什么默认关闭(性能考虑)

  • 6.7 AspectJ 编译时织入 vs Spring AOP 运行时代理

  • AspectJ 的三种织入方式

  • 对比 Spring AOP

  • 6.8 springboot3.x其AOP默认代理

  • 七、AOP 的应用场景

  • 7.1 日志记录(最常用)

  • 7.2 缓存处理

  • 7.3 权限校验

  • 八、AOP 失效的常见陷阱

  • 陷阱 1:同类方法调用(最高频)

  • 陷阱 2:非 public 方法

  • 陷阱 3:静态方法

  • 陷阱 4:final 方法

  • 陷阱 5:自调用(直接 new 对象)

  • 九、切面执行顺序

  • 9.1 @Order 注解

  • 9.2 Ordered 接口

  • 9.3 洋葱模型

  • 十、常见误区

  • 误区 1:认为 @Around 比 @Before 慢

  • 误区 2:认为 AOP 可以代理 static 方法

  • 误区 3:认为同类调用 @Transactional 生效

  • 误区 4:认为切面执行顺序只由 @Order 决定

  • 误区 5:认为 JDK 动态代理一定比 CGLIB 慢

  • 十一、最佳实践

    1. 优先使用 @Around 处理复杂逻辑
    1. 避免在切面中使用 @Autowired 的懒加载问题
    1. 同类调用优先拆分 Service
    1. 使用自定义注解代替复杂切点表达式
    1. 使用 exposeProxy 解决同类调用的注意事项
  • 十二、选型决策树

  • 十三、单元测试(结果)

  • 测试 1:AopProxyTest

  • 测试 2:AopFailureTest

  • 测试 3:AspectOrderTest

  • 十四、Spring 5 vs Spring 6 核心差异总结

  • 14.1 通知执行顺序变化

  • 14.2 默认代理策略变化

  • 14.3 迁移指南

  • 需要调整的地方

  • 不需要调整的地方

  • 14.4 版本选择建议

  • 十五、总结

  • 核心要点速查

  • 最佳实践清单

  • 十六、面试自测

  • 基础知识

  • Spring 5 vs Spring 6 差异专项

  • 十七、参考资料

  • Spring 官方文档

  • Spring 源码

  • JDK & CGLIB 源码

  • 书籍

  • 迁移指南


一、从一个实际问题说起

小 A 在开发一个电商系统时,遇到了这样的困扰:

复制代码
@Service public class OrderService { public void createOrder () { // 1. 日志记录 log.info( "创建订单开始" ); // 2. 权限检查 if (!userHasPermission()) { throw new SecurityException ( "无权限" ); } // 3. 事务控制 try { orderRepository.save(); } catch (Exception e) { throw e; } // 4. 日志记录 log.info( "创建订单结束" ); } public void cancelOrder () { // 重复的日志、权限、事务代码... } }

小 A 发现:

    1. 代码重复:每个方法都要写日志、权限、事务
    1. 难以维护:修改日志格式要改很多地方
    1. 业务不纯粹:业务逻辑里掺杂了太多非业务代码

这就是 AOP 要解决的问题。

后来,小 A 给 Service 加了 @Transactional,却发现 createUser() 内部调用 saveUser() 时,事务竟然不生效。查了半天才意识到:Spring AOP 基于代理,同类内部调用走的是 this,不经过代理对象。this和代理对象是两回事,this就是我们抛开spring框架,抛开代理对象,一早学习的java基础面向对象时的this,指代当前对象。

如果不知道 AOP 的底层原理,类似的"坑"会反复出现。


二、AOP 是什么?

2.1 概念定义

AOP(Aspect-Oriented Programming):面向切面编程,将横切关注点(日志、事务、权限、缓存等)从业务逻辑中分离出来,以声明式(对比编程式,直接使用注解的方式标注即可获得横切的功能,或者切点表达式)的方式织入到目标对象中。

说人话版本

想象一个公司的运作:

  • 核心业务 :产品开发、销售、客服(纵切)

  • 横切关注点 :财务报销、人事考勤、行政管理(横切)

AOP 就是把"财务、人事、行政"这些通用功能抽取出来,统一管理,而不是在每个部门内部都写一遍。

2.2 为什么需要 AOP

没有 AOP 之前,横切关注点的代码散布在各个业务方法中,导致:

  • 代码重复:每个方法都写一遍日志/权限/事务
  • 耦合严重:业务逻辑和非业务逻辑混在一起
  • 难以修改:修改日志格式要改所有方法

AOP 的解决方案:

  • 将横切关注点封装为切面(Aspect)
  • 通过切点表达式声明哪些方法需要被拦截(注解是其中一种方式)
  • 框架在运行时自动将切面逻辑织入目标方法(就是你看不见的地方框架会给你的添加功能,增强目标方法)

类比:没有 AOP 时,每个房间都要自己装水电。有了 AOP 后,水电管线统一铺设,房间只要声明"我需要水电"就行了。


三、AOP 核心术语

3.1 术语图解

3.2 术语详解

术语 英文 含义 示例
横切关注点 Cross-cutting Concern 横跨多个类的通用功能 日志、事务、权限、限流、统计、分布式锁
切面 Aspect 横切关注点的模块化 @Aspect 注解的类,一个形象的表达,聚合了其他横切要素
连接点 JoinPoint 可以插入切面的点 方法执行、异常抛出,就是某类里面的某个方法执行。被封装了的方法
切点 Pointcut 匹配连接点的条件 execution(service..*(..)) ,符合条件的方法集合
通知 Advice 在切点执行的动作 前置、后置、环绕,给目标方法的增强,在哪儿增强呢
目标对象 Target 被代理的对象 OrderService,this
代理对象 Proxy AOP 创建的对象 OrderService 的代理,通过实现接口(JDK)或继承目标对象的方式(cglib)
织入 Weaving 将切面应用到目标对象 创建代理的过程,因为有aop织入的需求,所以普通bean摇身一变成了代理后的bean

四、五种通知类型

4.1 通知类型执行顺序

重要提示:Spring 5 和 Spring 6 的通知执行顺序有重大差异!

Spring 5(Spring Boot 2.x)的执行顺序

Spring 5 顺序总结@Around-before → @Before → 目标方法 → @Around-after → @AfterReturning → @After


Spring 6(Spring Boot 3.x)的执行顺序

Spring 6 顺序总结@Around-before → @Before → 目标方法 → @AfterReturning → @After → @Around-after

关键变化 :Spring 6 中 @Around-after 移到了最后执行,这是为了更符合 AspectJ 的标准语义,让 @Around 真正"环绕"整个连接点(包括其他所有通知)。

注意

  • Spring 5 :@Around 的前置部分包裹了 @Before,@Around 的后置部分在 @AfterReturning 之前执行

  • Spring 6 :@Around 真正环绕整个连接点,@Around-after 在所有其他通知之后执行

  • 这是拦截器链的组织方式决定的(后续源码章节详解)

4.2 @Before 前置通知

执行时机:目标方法执行前。

复制代码
@Aspect @Component public class LogAspect { @Before("execution(* com.example.service.*.*(..))") public void before (JoinPoint jp) { String methodName = jp.getSignature().getName(); System.out.println( "方法执行前:" + methodName); Object[] args = jp.getArgs(); System.out.println( "参数:" + Arrays.toString(args)); } }

特点

  • 无法阻止目标方法执行
  • 无法修改返回值

使用场景:权限校验、参数验证、日志记录。


4.3 @AfterReturning 返回通知

执行时机:目标方法成功返回后。

复制代码
@Aspect @Component public class LogAspect { @AfterReturning( pointcut = "execution(* com.example.service.*.*(..))", returning = "result" ) public void afterReturning (JoinPoint jp, Object result) { System.out.println( "方法返回:" + result); } }

特点

  • 可以获取返回值
  • 无法修改返回值(修改也不影响调用方拿到的值)

使用场景:返回结果日志、数据脱敏、审计。


4.4 @AfterThrowing 异常通知

执行时机:目标方法抛出异常后。

复制代码
@Aspect @Component public class LogAspect { @AfterThrowing( pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex" ) public void afterThrowing (JoinPoint jp, Exception ex) { System.out.println( "方法异常:" + ex.getMessage()); } }

特点

  • 可以获取异常对象
  • 无法阻止异常向上传播(除非在通知内 catch 并吞掉)

使用场景:异常日志、错误告警。


4.5 @After 最终通知

执行时机:目标方法执行后(无论成功或失败)。

复制代码
@Aspect @Component public class LogAspect { @After("execution(* com.example.service.*.*(..))") public void after (JoinPoint jp) { System.out.println( "方法执行完毕" ); } }

特点

  • 类似 try-catch-finally 的 finally
  • 无法获取返回值或异常

使用场景:资源清理、连接关闭。


4.6 @Around 环绕通知(最强大)

执行时机:包裹目标方法执行。

复制代码
@Aspect @Component public class LogAspect { @Around("execution(* com.example.service.*.*(..))") public Object around (ProceedingJoinPoint pjp) throws Throwable { // 前置逻辑 System.out.println( "方法执行前" ); try { // 执行目标方法 Object result = pjp.proceed(); // 后置逻辑 System.out.println( "方法执行后" ); return result; } catch (Throwable e) { // 异常处理 System.out.println( "方法异常" ); throw e; } } }

特点

  • 可以控制是否执行目标方法(不调用 proceed() 即可阻止)
  • 可以修改返回值
  • 可以捕获异常
  • 功能最全,一个顶五个

使用场景:性能监控、缓存处理、事务控制、重试机制。


4.7 通知类型对比表

通知类型 执行时机 能否获取参数 能否获取返回值 能否阻止执行 能否修改返回值
@Before 方法执行前
@AfterReturning 方法返回后
@AfterThrowing 方法异常后
@After 方法结束后
@Around 包裹方法执行

五、切点表达式详解

5.1 execution 表达式(最常用)

复制代码
// 语法 execution(返回类型 类路径.方法名 (参数)) // 示例 @Before("execution(* com.example.service.UserService.*(..))") // 匹配 UserService 的所有方法 @Before("execution(* com.example.service.*.*(..))") // 匹配 service 包下所有类的所有方法 @Before("execution(* com.example.service..*.*(..))") // 匹配 service 包及其子包下所有类的所有方法 @Before("execution(public * *(..))") // 匹配所有 public 方法 @Before("execution(* set*(..))") // 匹配所有以 set 开头的方法

通配符说明

  • *:匹配任意字符(一个层级)
  • ..:匹配任意字符(多个层级)
  • (..):匹配任意参数

5.2 within 表达式

复制代码
@Before("within(com.example.service.UserService)") // 匹配指定类型 @Before("within(com.example.service.*)") // 匹配指定包 @Before("within(com.example.service..*)") // 匹配指定包及其子包

与 execution 的区别

  • execution:匹配方法(粒度更细)
  • within:匹配类型(粒度更粗)
  • 到包,到类层级,不用细化到方法

5.3 @annotation 表达式

  • 常用,自定义AOP时便捷使用,灵活;按需标记

  • 就是给目标方法/类,贴贴标签,声明式编程很大程度源于注解标注

    @Before("@annotation(org.springframework.transaction.annotation.Transactional)") // 匹配带有 @Transactional 注解的方法 @Before("@annotation(com.example.annotation.Loggable)") // 匹配带有自定义注解的方法

实战案例:自定义注解 + 切面

复制代码
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Loggable { } @Aspect @Component public class LogAspect { @Around("@annotation(Loggable)") public Object log (ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); System.out.println(pjp.getSignature() + " 耗时 " + (System.currentTimeMillis() - start) + "ms" ); return result; } }

5.4 组合表达式

  • 更强大,灵活,复用

  • 各种方式使用且或非的方式获得更强的组合效果

    // AND(且) @Before("execution(* com.example.service..(..)) && @annotation(Loggable)") // OR(或) @Before("execution(* com.example.service..(..)) || execution(* com.example.controller..(..))") // NOT(非) @Before("execution(* com.example.service..(..)) && !execution(* com.example.service.TestService.*(..))")


六、AOP 底层原理(核心章节)

6.1 两种代理方式(版本也有差异!)

Spring Boot 版本变化

  • Spring Boot 2.x(Spring 5):默认根据是否有接口智能选择(有接口→JDK,无接口→CGLIB)
  • Spring Boot 3.x(Spring 6)默认全部使用 CGLIBspring.aop.proxy-target-class=true
  • 不需要实现接口
  • 对开发者更透明
  • 性能在现代 JVM 上已无明显差异

SpringBoot 3.2.x文档:

JDK 17+ 兼容性 :CGLIB 基于字节码生成代理类,在 JDK 16+ 模块化系统(JPMS)下可能遇到 InaccessibleObjectException。Spring Boot 3.x(基于 JDK 17+)中,如果 CGLIB 代理失败,可考虑以下方案:(1)使用 JDK 动态代理(为类实现接口);(2)升级 CGLIB 到最新版本或使用 ByteBuddy 替代;(3)添加 JVM 参数 --add-opens java.base/java.lang=ALL-UNNAMED


6.2 JDK 动态代理:完整源码分析

JDK 动态代理的核心类java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler

Proxy.newProxyInstance 内部做了什么
复制代码
// JDK 源码简化版(java.lang.reflect.Proxy) public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) { // 1. 校验 interfaces 数组中是否有非接口类型 for (Class<?> intf : interfaces) { if (!intf.isInterface()) { throw new IllegalArgumentException (); } } // 2. 查找或生成代理类(核心!) Class<?> proxyClass = getProxyClass0(loader, interfaces); // 3. 获取代理类的构造函数(参数为 InvocationHandler) Constructor<?> cons = proxyClass.getConstructor( new Class <?>[] { InvocationHandler.class }); // 4. 创建代理实例,传入 InvocationHandler return cons.newInstance( new Object [] { h }); }

关键步骤详解

步骤 1getProxyClass0 内部做了三件事:

  • 检查 interfaces 是否超过 65535 个(JVM 限制)
  • proxyClassCache 中查找是否已存在该代理类(缓存避免重复生成)
  • 调用 ProxyGenerator.generateProxyClass 生成字节码

步骤 2:生成的代理类长什么样?(反编译后的示例)

复制代码
// 代理类 $Proxy0(JVM 动态生成的字节码,反编译后大致如此) public final class $Proxy0 extends Proxy implements UserService { private Method m1; // equals private Method m2; // hashCode private Method m3; // saveUser(目标接口方法) private Method m4; // getUser public $Proxy0(InvocationHandler h) { super (h); // 保存在 Proxy.h 中 } @Override public final String saveUser (String name) { // 关键!将方法调用委派给 InvocationHandler。这个方式的高明之处在于将代理的增强逻辑分离封装抽象了 // 为什么要把this自己传入进去,这里有点像传递上下文的感觉,就是代理对象本身的引用,没准增强逻辑的对象有用到。 return (String) super .h.invoke( this , m3, new Object []{name}); } @Override public final String getUser ( int id) { return (String) super .h.invoke( this , m4, new Object []{id}); } }

为什么 JDK 动态代理只能代理接口?

因为 Java 的单继承机制:代理类已经继承了 java.lang.reflect.Proxy,无法再继承目标类。所以只能通过实现接口来"冒充"目标对象。

进一步拓展思考!

这个代理对象,在spring体系下,IOC容器中,是一个bean,是我们平时依赖注入的bean,被增强过的bean,这样的话,容器中是不是有一大半代理对象bean都是Proxy的子类,是吧,代理老祖广收门徒,只要你有实现接口,我就能代理你,JDK动态代理如是说。

类比:JDK 动态代理就像"替身演员"------演员必须提前签好合同(定义接口),替身才能照着合同演。如果演员没签合同(没有接口),替身就不知道要演什么。


InvocationHandler.invoke 如何分发到目标方法

InvocationHandler代表执行JDK代理分离出来的增强逻辑。这个方式的高明之处在于将代理的增强逻辑分离封装抽象了;

而AopInvocationHandler是spring的InvocationHandler实现,关键在于Spring自己通过使用拦截器链的思想将多个执行逻辑也抽象封装了(并且按Order顺序执行)。

总结:JDK封装一次,Spring框架封装一次;接口就是抽象,就是与实现分离,达到扩展,方便业务差异化实现多个增强逻辑。

复制代码
// Spring 内部的 InvocationHandler 实现(AopInvocationHandler 简化版) class AopInvocationHandler implements InvocationHandler { private final TargetSource targetSource; private final List<Object> chain; // 拦截器链 @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { // 1. 如果是 equals / hashCode,直接处理 if (AopUtils.isEqualsMethod(method)) { return proxy == args[ 0 ]; } if (AopUtils.isHashCodeMethod(method)) { return hashCode(); } // 2. 构建拦截器链,这个属于Spring框架层将众多的aop增强逻辑封装,因为该切点(目标方法)有可能存在多个增强逻辑(日志、事务、权限等) List<Object> chain = this .advised.getInterceptorsAndDynamicInterceptionAdvice( method, targetClass); // 3. 创建 MethodInvocation,执行责任链 MethodInvocation invocation = new ReflectiveMethodInvocation ( proxy, target, method, args, targetClass, chain); // 4. 从链头开始执行 return invocation.proceed(); } }

完整执行流程

6.3 CGLIB 代理:完整源码分析

CGLIB 的核心类net.sf.cglib.proxy.Enhancernet.sf.cglib.proxy.MethodInterceptornet.sf.cglib.proxy.MethodProxy

Enhancer.create 如何生成子类
复制代码
// CGLIB 源码简化版(Enhancer.create 流程) public class Enhancer { public Object create () { // 1. 确定父类(目标类) if (superClass == null ) { throw new IllegalStateException ( "No superclass specified" ); } // 2. 使用 ASM 字节码框架生成子类 //    子类名 = 目标类名 + "$$EnhancerBySpringCGLIB$$" + hashCode,这是spring5;spring6则是"$$SpringCGLIB$$" Class<?> enhanced = ReflectUtils.defineClass( this .useFactory ? factoryName : null , this .superClass, this .interfaces, this .loadUseWeakClassCache, this .generator // 字节码生成器 ); // 3. 通过反射创建子类实例 return ReflectUtils.newInstance( enhanced, this .constructorArgs, this .constructor ); } }

生成的 CGLIB 子类长什么样?(反编译后)

  • spring5,springboot2.x,类名包括$$EnhancerBySpringCGLIB$$

  • spring6,springboot3.x,类名调整为包括$$SpringCGLIB$$

    // CGLIB 生成的子类(OrderService$$EnhancerBySpringCGLIB$$12345) public class OrderService$$EnhancerBySpringCGLIB$$12345 extends OrderService { // 回调数组 private MethodInterceptor CGLIBCALLBACK_0; @Override public String createOrder (String productId) { MethodInterceptor cb = this .CGLIBCALLBACK_0; if (cb != null ) { // 关键!将方法调用委派给 MethodInterceptor Object result = cb.intercept( this , CGLIBcreateOrder0Method, new Object []{productId}, CGLIBcreateOrder0Proxy); return (String) result; } return super .createOrder(productId); } // CGLIB 为每个方法预计算 MethodProxy private static Method CGLIBcreateOrder0Method; private static MethodProxy CGLIBcreateOrder0Proxy; static { // 通过 Signature 创建 MethodProxy CGLIBcreateOrder0Method = ReflectUtils.findMethods( "createOrder" , ...)[ 0 ]; CGLIBcreateOrder0Proxy = MethodProxy.create( OrderService.class, // 父类 OrderService$$EnhancerBySpringCGLIB$$ 12345. class, // 子类 "createOrder" ); } }

MethodInterceptor.intercept 的拦截流程
复制代码
// Spring 内部的 DynamicAdvisedInterceptor(简化版) class DynamicAdvisedInterceptor implements MethodInterceptor { @Override public Object intercept (Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 1. 获取该方法的拦截器链 List<Object> chain = this .advised.getInterceptorsAndDynamicInterceptionAdvice( method, targetClass); // 2. 创建 CglibMethodInvocation CglibMethodInvocation invocation = new CglibMethodInvocation ( proxy, target, method, args, targetClass, chain, methodProxy); // 3. 执行责任链 return invocation.proceed(); } }
invokeSuper vs invoke 的区别

这是 CGLIB 中一个容易踩坑的地方:

复制代码
// MethodProxy 简化源码 public class MethodProxy { // invoke:调用目标对象的原始方法 public Object invoke (Object target, Object[] args) throws Throwable { // 通过反射调用 target 上的方法 // 如果 target 是代理对象,会再次触发拦截! return invokeInit.get().invoke(target, args); } // invokeSuper:调用父类(即被代理的原始类)的方法 public Object invokeSuper (Object obj, Object[] args) throws Throwable { // 通过 FastClass 机制直接调用父类方法 // 不经过反射,不走拦截器,更快 return invokeSuperInit.get().invoke(obj, args); } }

关键区别

方法 调用目标 是否经过拦截器 性能
invoke 目标对象(可能是代理) 可能再次触发拦截 反射,较慢,这是统一操作
invokeSuper 父类原始方法 不经过拦截器 FastClass 直接调用,更快,因为有些方法不用代理

一个类中有@Transactional事务注解,那么这个目标对象bean就会多出个代理对象bean,而最终依赖注入的这个bean则变为代理对象(会持有目标对象的引用)。但是原本对象上只有个别方法是添加了事务注解的,而查询类的方法一般不会加,所以在处理的时候代理对象bean选择直接调用目标对象也就是原始父类的方法invokeSuper,省了一些反射操作。

为什么 invokeSuper 更快?

CGLIB 使用了 FastClass 机制:为每个方法生成一个索引表,通过索引直接调用方法,避免了反射开销。本质上是把反射调用变成了类似 switch-case 的直接调用。

复制代码
// FastClass 简化示意 class FastClassForOrderService { public Object invoke ( int index, Object obj, Object[] args) { OrderService self = (OrderService) obj; switch (index) { case 0 : return self.createOrder((String) args[ 0 ]); // 直接调用,无反射 case 1 : return self.getOrder((Integer) args[ 0 ]); default : throw new IllegalArgumentException (); } } }
final 方法为什么不能被 CGLIB 代理

CGLIB 通过生成目标类的子类来实现代理,子类必须重写父类方法才能插入拦截逻辑。final 方法不能被重写,所以 CGLIB 无法为 final 方法生成代理逻辑。

复制代码
// final 方法在 CGLIB 子类中不会被重写 public class OrderService$$EnhancerBySpringCGLIB$$12345 extends OrderService { // createOrder 被重写 -> 有拦截逻辑 @Override public String createOrder (String productId) { // ... 拦截逻辑 ... } // finalMethod 不被重写 -> 直接继承父类实现 // 所以 final 方法的调用不经过 MethodInterceptor }

6.4 拦截器链执行原理(责任链模式)

这是 Spring AOP 最核心的机制之一。

CglibMethodInvocation.proceed 的责任链模式
复制代码
// ReflectiveMethodInvocation 简化源码(JDK 代理和 CGLIB 代理共用类似逻辑) public class ReflectiveMethodInvocation implements MethodInvocation { protected final Object proxy; protected final Object target; protected final Method method; protected final Object[] arguments; protected List<Object> interceptorsAndDynamicMethodMatchers; // 当前执行到的拦截器索引 private int currentInterceptorIndex = - 1 ; @Override public Object proceed () throws Throwable { // 1. 所有拦截器都已执行,调用目标方法 if ( this .currentInterceptorIndex == this .interceptorsAndDynamicMethodMatchers.size() - 1 ) { return invokeJoinpoint(); // method.invoke(target, arguments) } // 2. 获取下一个拦截器 Object interceptorOrInterceptionAdvice = this .interceptorsAndDynamicMethodMatchers.get(++ this .currentInterceptorIndex); // 3. 是 InterceptorAndDynamicMethodMatcher?做参数匹配 if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; if (dm.methodMatcher.matches(method, targetClass, arguments)) { return dm.interceptor.invoke( this ); // 匹配成功,执行拦截器 } return proceed(); // 匹配失败,跳过 } // 4. MethodInterceptor,执行拦截器(会再次调用 this.proceed()) return ((MethodInterceptor) interceptorOrInterceptionAdvice) .invoke( this ); } protected Object invokeJoinpoint () throws Throwable { return AopUtils.invokeJoinpointUsingReflection(target, method, arguments); } }

执行流程图
关键理解

  • proceed() 每次将 currentInterceptorIndex 加 1
  • 每个 MethodInterceptor.invoke 内部调用 invocation.proceed() 才能继续链的后续节点
  • @Around 必须调用 pjp.proceed(),否则拦截器链在当前位置中断,后面的通知和目标方法都不会执行

类比 :拦截器链像一条流水线,proceed() 是把产品传送到下一道工序。如果某个工序扣留了产品(不调用 proceed),后面的工序就收不到。

多个通知如何组成拦截器链

Spring 将每种通知类型封装为对应的拦截器:

通知类型 拦截器实现类
@Around AspectJAroundAdvice → MethodInterceptor
@Before AspectJMethodBeforeAdvice → MethodInterceptor
@AfterReturning AspectJAfterReturningAdvice → MethodInterceptor
@AfterThrowing AspectJAfterThrowingAdvice → MethodInterceptor
@After AspectJAfterAdvice → MethodInterceptor

所有通知都被包装为 MethodInterceptor,按顺序加入拦截器链,由 proceed() 统一调度。方法拦截器,还挺形象的,本质确实就是给目标方法前前后后,左左右右增强。


6.5 AnnotationAwareAspectJAutoProxyCreator 工作原理

这是 Spring AOP 的"幕后黑手"------所有代理的创建都经过它。

从 @EnableAspectJAutoProxy 到注册

@EnableAspectJAutoProxy 源码

复制代码
@Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { boolean proxyTargetClass () default false ; // 是否强制 CGLIB,true表示cglig,false表示有接口就jdk,无则cglib;springboot3中yml配置优先级更高!!! boolean exposeProxy () default false ; // 是否暴露代理到 AopContext }

AspectJAutoProxyRegistrar 在容器注册了一个 AnnotationAwareAspectJAutoProxyCreator

一个非常强大的bean,他是其他普通bean的代理检查员,所有bean都需要给他检验判断一下,是否需要升级替换为代理bean。

postProcessAfterInitialization 中如何判断是否需要创建代理
复制代码
// AbstractAutoProxyCreator.postProcessAfterInitialization 简化流程 @Override public Object postProcessAfterInitialization ( @Nullable Object bean, String beanName) { if (bean != null ) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (! this .earlyProxyReferences.contains(cacheKey)) { // 核心:决定是否创建代理 return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } protected Object wrapIfNecessary (Object bean, String beanName, Object cacheKey) { // 1. 跳过:已处理过、infra 类、shouldSkip 的类 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { return bean; } // 2. 获取所有适用于该 Bean 的 Advisor(切面通知器) Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null ); // 3. 如果有 Advisor 适用于该 Bean,创建代理 if (specificInterceptors != DO_NOT_PROXY) { this .advisedBeans.put(cacheKey, Boolean.TRUE); // 创建代理 return createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource (bean)); } return bean; }

AspectJ Advisor 的解析和排序

复制代码
// AnnotationAwareAspectJAdvisorFactory 简化流程 public List<Advisor> getAdvisors (MetadataAwareAspectInstanceFactory aspectInstanceFactory) { List<Advisor> advisors = new ArrayList <>(); // 遍历 @Aspect 类中的所有方法 for (Method method : getAdvisorMethods(aspectClass)) { // 跳过 @Pointcut 标注的方法 if (isPointcut(method)) continue ; // 解析通知类型(@Before / @Around / @After 等) AspectJAnnotation<?> annotation = getAspectJAnnotation(method); if (annotation == null ) continue ; // 创建对应的 Advisor Advisor advisor = createAdvisor(method, declarationOrder, aspectInstanceFactory); advisors.add(advisor); } // 按 @Order 排序 AnnotationAwareOrderComparator.sort(advisors); return advisors; }

6.6 @EnableAspectJAutoProxy(exposeProxy=true) 底层实现

AopContext 的 ThreadLocal 机制
复制代码
// org.springframework.aop.framework.AopContext 源码,版本:spring6.x public final class AopContext { private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal ( "Current AOP proxy" ); private AopContext () { } public static Object currentProxy () throws IllegalStateException { Object proxy = currentProxy.get(); if (proxy == null ) { throw new IllegalStateException ( "Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context." ); } else { return proxy; } } @Nullable static Object setCurrentProxy ( @Nullable Object proxy) { Object old = currentProxy.get(); if (proxy != null ) { currentProxy.set(proxy); } else { currentProxy.remove(); } return old; } }
currentProxy() 如何获取当前代理

在代理对象的方法调用入口处,Spring 会将代理对象设置到 AopContext 中:

复制代码
// CglibAopProxy.DynamicAdvisedInterceptor 简化 class DynamicAdvisedInterceptor implements MethodInterceptor { @Override public Object intercept (Object proxy, Method method, Object[] args, MethodProxy methodProxy) { Object oldProxy = null ; if ( this .advised.exposeProxy) { // 将当前代理设置到 ThreadLocal oldProxy = AopContext.setCurrentProxy(proxy); } try { // 执行拦截器链... Object result = proceedChain(); return result; } finally { // 清理 ThreadLocal,避免内存泄漏 if ( this .advised.exposeProxy) { AopContext.setCurrentProxy(oldProxy); } } } }
setCurrentProxy(oldProxy) 设计哲学

关键设计:setCurrentProxy(oldProxy),为什么不使用remove()?

核心原因:支持嵌套代理调用

场景示例

复制代码
@Service public class ServiceA { @Autowired private ServiceB serviceB; @Transactional public void methodA () { // 此时 AopContext 中存储的是 ServiceA 的代理 serviceB.methodB(); // 嵌套调用 ServiceB } } @Service public class ServiceB { @Transactional public void methodB () { // 此时 AopContext 中应该存储的是 ServiceB 的代理 Object proxy = AopContext.currentProxy(); // 需要获取 ServiceB 的代理 } }

执行流程详解

1. 进入 ServiceA.methodA()

复制代码
oldProxy = null ; // 之前没有代理 AopContext.setCurrentProxy(ServiceA的代理); // 设置 ServiceA 代理 // ThreadLocal: [ServiceA代理]

2. 嵌套调用 ServiceB.methodB()

复制代码
oldProxy = ServiceA的代理; // 保存外层代理 AopContext.setCurrentProxy(ServiceB的代理); // 设置 ServiceB 代理 // ThreadLocal: [ServiceB代理] (覆盖了 ServiceA 代理) // 执行 ServiceB 的业务逻辑 Object proxy = AopContext.currentProxy(); // 获取到 ServiceB 的代理 ✅

3. ServiceB.methodB() 执行完毕,清理资源

复制代码
AopContext.setCurrentProxy(ServiceA的代理); // 恢复为 ServiceA 的代理 ✅ // ThreadLocal: [ServiceA代理] (恢复外层上下文) // 而不是 remove(),因为外层还在使用中

4. ServiceA.methodA() 执行完毕,清理资源

复制代码
AopContext.setCurrentProxy( null ); // oldProxy 是 null,相当于 remove // ThreadLocal: [] (完全清空)

为什么不能用 remove()?

如果使用 remove()

复制代码
// ServiceB.methodB() 执行完毕 if (setProxyContext) { AopContext.currentProxy.remove(); // ❌ 错误! }

问题 :此时 ServiceA.methodA() 还没有执行完,它还需要通过 AopContext.currentProxy() 获取自己的代理对象。如果直接 remove(),ServiceA 后续调用 AopContext.currentProxy() 就会抛出 IllegalStateException

setCurrentProxy 的设计精妙之处

看源码:

复制代码
@Nullable static Object setCurrentProxy ( @Nullable Object proxy) { Object old = currentProxy.get(); // 保存旧值 if (proxy != null ) { currentProxy.set(proxy); // 设置新值 } else { currentProxy.remove(); // proxy 为 null 时才 remove } return old; // 返回旧值,供恢复使用 }

关键设计

  • 传入 null 时才会真正 remove()
  • 传入具体对象时会 set()
  • 始终返回旧值,形成"栈"的效果
类比理解:函数调用栈

这就像函数调用栈的压栈和弹栈:

复制代码
methodA() 调用 → 压栈 ServiceA 代理 methodB() 调用 → 压栈 ServiceB 代理 methodB() 返回 → 弹栈,恢复 ServiceA 代理 methodA() 返回 → 弹栈,清空

如果用 remove() 就相当于直接把整个栈清空了,外层的上下文就丢失了。

总结对比
方式 效果 适用场景
setCurrentProxy(oldProxy) 恢复上一层代理 ✅ 支持嵌套调用
currentProxy.remove() 完全清空 ❌ 会破坏外层上下文

所以这里必须用 setCurrentProxy(oldProxy)恢复调用前的状态 ,而不是简单地删除。这是典型的栈式管理思想。

为什么默认关闭(性能考虑)
  • ThreadLocal.set/get 有开销
  • 每次方法调用都要设置和清理
  • 大多数场景不需要同类调用访问代理

类比exposeProxy 就像在每间办公室门口挂一个"当前代理人"的牌子。默认不挂(省成本),需要的时候再挂。


6.7 AspectJ 编译时织入 vs Spring AOP 运行时代理

AspectJ 的三种织入方式
织入方式 时机 说明
编译时织入(CTW) javac 编译期 使用 ajc 编译器,在编译时将切面代码直接写入目标类字节码
后编译时织入(Post-compile Weaving) .class 文件生成后 对已有的 .class 或 .jar 进行织入
加载时织入(LTW) ClassLoader 加载时 通过 -javaagent 在类加载时修改字节码
对比 Spring AOP
特性 Spring AOP AspectJ
织入时机 运行时(代理) 编译时/加载时
代理方式 JDK/CGLIB 直接修改字节码
方法级别
构造器级别
字段级别 ✅(field get/set)
static 方法
final 方法 ❌(CGLIB)
同类调用 ❌(不经过代理) ✅(直接织入)
性能 有代理开销 无代理开销
使用复杂度 低(注解即可) 高(需要 ajc 或 LTW agent)

Spring AOP 为什么只支持方法级别的切面?

因为 Spring AOP 基于代理,代理只能拦截"通过代理对象调用方法"这个行为。字段访问、构造器调用、静态方法都不走代理对象的方法调用路径,所以无法拦截。

类比

  • Spring AOP 像"门卫"------只能拦截从大门进出的人(方法调用)
  • AspectJ 像"隐形改造"------直接把规则写入建筑内部(字节码),任何角落都能覆盖

6.8 springboot3.x其AOP默认代理

元数据:spring-configuration-metadata.json

具体默认值:

复制代码
{ "name" : "spring.aop.auto" , "type" : "java.lang.Boolean" , "description" : "Add @EnableAspectJAutoProxy." , "defaultValue" : true } , { "name" : "spring.aop.proxy-target-class" , "type" : "java.lang.Boolean" , "description" : "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false)." , "defaultValue" : true } ,

所以application.yml,默认就是true,使用cglib;不想则得显式改为false

复制代码
# Spring AOP 配置 spring: aop: # 强制使用 JDK 动态代理(目标类有接口时) # proxy-target-class: true 表示使用 CGLIB(Spring Boot 3.x 默认) # proxy-target-class: false 表示优先使用 JDK 动态代理 proxy-target-class: false

七、AOP 的应用场景

7.1 日志记录(最常用)

复制代码
@Aspect @Component public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); @Around("@annotation(Loggable)") public Object logExecutionTime (ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); log.info( "方法执行:{}.{}" , pjp.getSignature().getDeclaringTypeName(), pjp.getSignature().getName()); try { Object result = pjp.proceed(); long elapsed = System.currentTimeMillis() - start; log.info( "方法完成:{}ms" , elapsed); return result; } catch (Throwable e) { log.error( "方法异常" , e); throw e; } } }

7.2 缓存处理

复制代码
@Aspect @Component public class CacheAspect { @Autowired private CacheManager cacheManager; @Around("@annotation(Cacheable)") public Object cacheable (ProceedingJoinPoint pjp) throws Throwable { String key = generateKey(pjp); Object cached = cacheManager.get(key); if (cached != null ) { return cached; // 缓存命中,不调用 proceed() } Object result = pjp.proceed(); cacheManager.put(key, result); return result; } }

7.3 权限校验

复制代码
@Aspect @Component public class AuthAspect { @Around("@annotation(RequiresPermission)") public Object checkPermission (ProceedingJoinPoint pjp) throws Throwable { MethodSignature sig = (MethodSignature) pjp.getSignature(); RequiresPermission anno = sig.getMethod().getAnnotation(RequiresPermission.class); if (!hasPermission(anno.value())) { throw new AccessDeniedException ( "无权限:" + anno.value()); } return pjp.proceed(); } }

八、AOP 失效的常见陷阱

陷阱 1:同类方法调用(最高频)

复制代码
@Service public class UserService { public void createUser () { // 直接调用 saveUser,AOP 不生效! saveUser(); } @Transactional public void saveUser () { userRepository.save(); } }

问题原因

  • Spring AOP 基于代理
  • 同类方法调用是 this.saveUser(),走的是目标对象自身,不经过代理
  • 所以 saveUser() 的事务注解不生效

类比:你雇了一个秘书(代理)帮你处理文件,但你给自己递文件时,秘书看不到。

解决方案

复制代码
// 方案 1:注入自己(推荐,最简洁) @Service public class UserService { @Autowired @Lazy private UserService self; public void createUser () { self.saveUser(); // 通过代理调用 } @Transactional public void saveUser () { userRepository.save(); } } // 方案 2:AopContext(需要 exposeProxy=true) @Service public class UserService { public void createUser () { ((UserService) AopContext.currentProxy()).saveUser(); } @Transactional public void saveUser () { userRepository.save(); } } // 方案 3:拆分到不同 Service(最干净) @Service class UserService { @Autowired private UserSaveService saveService; public void createUser () { saveService.saveUser(); // 跨 Bean 调用,走代理 } } @Service class UserSaveService { @Transactional public void saveUser () { userRepository.save(); } }

陷阱 2:非 public 方法

复制代码
@Service public class MyService { // ❌ 不生效 @Transactional private void privateMethod () {} // ❌ 不生效(Spring AOP 默认只代理 public,CGLIB 下 protected 部分支持) @Transactional protected void protectedMethod () {} // ✅ 生效 @Transactional public void publicMethod () {} }

原因:Spring AOP 的代理只对 public 方法保证完整支持。private 方法不可见,protected 方法在 JDK 代理下也不可用。


陷阱 3:静态方法

复制代码
@Service public class MyService { // ❌ 不生效 @Transactional public static void staticMethod () {} }

原因:static 方法属于类,不属于对象,无法通过代理对象调用。


陷阱 4:final 方法

复制代码
@Service public class MyService { // ❌ 不生效(CGLIB 无法继承 final 方法) @Transactional public final void finalMethod () {} }

原因:CGLIB 通过继承实现代理,final 方法无法被重写。


陷阱 5:自调用(直接 new 对象)

复制代码
// ❌ 直接 new,不是代理 OrderService service = new OrderService (); service.createOrder(); // 事务不生效! // ✅ 从容器获取 @Autowired private OrderService service; // 这是代理对象 service.createOrder(); // 事务生效

九、切面执行顺序

9.1 @Order 注解

复制代码
@Aspect @Order(1) // 优先级高,先执行 @Component public class AuthAspect { @Around("...") public Object auth (ProceedingJoinPoint pjp) throws Throwable { System.out.println( "1. 权限检查" ); return pjp.proceed(); } } @Aspect @Order(2) // 优先级低,后执行 @Component public class LogAspect { @Around("...") public Object log (ProceedingJoinPoint pjp) throws Throwable { System.out.println( "2. 日志记录" ); return pjp.proceed(); } }

规则@Order 值越小,优先级越高,越先执行其 @Around-before@Before

9.2 Ordered 接口

复制代码
@Aspect @Component public class MyAspect implements Ordered { @Override public int getOrder () { return 1 ; } }

9.3 洋葱模型

多个切面的环绕通知形成"洋葱"结构:

复制代码
Order(1)-before → Order(2)-before → 目标方法 → Order(2)-after → Order(1)-after

外层切面的 before 先执行,但 after 后执行(逆序回退)。


十、常见误区

误区 1:认为 @Around 比 @Before 慢

不一定! @Around 实际上性能更好:

  • @Around 只创建一个通知实例
  • @Before + @AfterReturning + @AfterThrowing 组合会产生多个通知实例和多次拦截器链遍历
  • @Around 一次拦截搞定所有逻辑,拦截器链更短

实测:在高频调用场景下,单个 @Around 通常比等效的 @Before + @AfterReturning 快 10%-20%。


误区 2:认为 AOP 可以代理 static 方法

不能! static 方法属于类,不属于对象实例。代理对象只能拦截通过它调用的实例方法。

复制代码
// ❌ 以下切面不会生效 @Aspect public class MyAspect { @Before("execution(static * com.example.Service.myStaticMethod(..))") public void before () {} // 永远不会触发 }

如果需要拦截静态方法,必须使用 AspectJ 的编译时织入(CTW)。


误区 3:认为同类调用 @Transactional 生效

不生效! 这是 Spring AOP 最常见的陷阱(见第八章陷阱 1)。

同类内部调用走 this,不经过代理。解决方案:

    1. 拆分到不同 Service
    1. 注入 @Lazy 的自己
    1. AopContext.currentProxy()

误区 4:认为切面执行顺序只由 @Order 决定

不完整! 切面排序受多种因素影响:

排序方式 优先级
@Order 注解
实现 Ordered 接口
@jakarta.annotation.Priority
未标注任何排序 默认 Ordered.LOWEST_PRECEDENCE (最后)

实际排序规则AnnotationAwareOrderComparator):

    1. 优先使用 @Order 的 value
    1. 其次使用 Ordered.getOrder()
    1. 最后使用 @Priority
    1. 都没有则排到最后

误区 5:认为 JDK 动态代理一定比 CGLIB 慢

过时认知! 在 JDK 8+ 之后:

  • JDK 动态代理使用 LambdaMetafactory 生成字节码,性能大幅提升
  • CGLIB 因为要生成子类字节码,初始化开销更大
  • 高频调用场景下,两者性能差异已很小

Spring Boot 3.x 默认 CGLIB 的原因:不是性能,而是更方便------不需要定义接口。


十一、最佳实践

1. 优先使用 @Around 处理复杂逻辑

复制代码
// ✅ 推荐:一个 @Around 搞定所有 @Around("execution(* com.example.service.*.*(..))") public Object around (ProceedingJoinPoint pjp) throws Throwable { long start = System.nanoTime(); try { Object result = pjp.proceed(); log.info( "成功:{} {}ns" , pjp.getSignature(), System.nanoTime() - start); return result; } catch (Throwable e) { log.error( "异常:{}" , pjp.getSignature(), e); throw e; } } // ❌ 不推荐:@Before + @AfterReturning + @AfterThrowing 分开写

2. 避免在切面中使用 @Autowired 的懒加载问题

复制代码
@Aspect @Component public class MyAspect { // 切面本身也是 Bean,@Autowired 正常使用 @Autowired private SomeService someService; }

3. 同类调用优先拆分 Service

复制代码
// ✅ 推荐:拆分到不同 Bean @Service class OrderCreateService { @Autowired private OrderSaveService saveService; public void create () { saveService.save(); // 走代理 } } @Service class OrderSaveService { @Transactional public void save () { ... } } // ⚠️ 备选:注入自己(有循环依赖风险) @Autowired @Lazy private SelfService self;

4. 使用自定义注解代替复杂切点表达式

复制代码
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Audit { String action () default "" ; } @Aspect @Component public class AuditAspect { @Around("@annotation(audit)") public Object audit (ProceedingJoinPoint pjp, Audit audit) throws Throwable { // 直接获取注解参数 System.out.println( "操作:" + audit.action()); return pjp.proceed(); } }

5. 使用 exposeProxy 解决同类调用的注意事项

复制代码
# application.properties spring.aop.auto-proxy-expose-proxy = true

// 使用 AopContext.currentProxy() 时注意: // 1. 有 ThreadLocal 开销 // 2. 非代理环境下会抛 IllegalStateException // 3. 方法结束后自动清理,不需要手动 remove

十二、选型决策树

JDK vs CGLIB 对比

维度 JDK 动态代理 CGLIB
代理方式 实现接口 继承子类
需要接口
final 类 不影响 ❌ 无法代理
final 方法 不影响 ❌ 无法代理
private 方法 不涉及 ❌ 无法代理
性能 好(FastClass)
初始化开销 中(生成字节码)
Spring Boot 2.x 默认 ✅(有接口时) ✅(无接口时)
Spring Boot 3.x 默认 ❌(需配置) ✅(强制默认)

十三、单元测试(结果)

以下为测试结果,具体代码基于 Spring Boot 3.x + Spring 6,可直接在项目中运行,验证 AOP 的各种行为。

代码:Spring AOP 原理全解析(单元测试代码),处于副文章位置

重要说明 :本文档的测试代码已针对 Spring Boot 3.x(Spring 6)进行调整。如果你使用的是 Spring Boot 2.x(Spring 5),需要注意以下差异:

  • 通知执行顺序 :Spring 5 中 @Around-after@AfterReturning 之前执行

  • 默认代理策略 :Spring Boot 2.x 默认根据接口智能选择 JDK/CGLIB,而 3.x 默认全部使用 CGLIB

  • CGLIB 类名格式 :Spring Boot 2.x 为 $$EnhancerBySpringCGLIB$$,3.x 为 $$SpringCGLIB$$

  • 具体单元测试代码,请查看第二篇文章:《单元测试 | Spring AOP原理全解析》

测试 1:AopProxyTest

测试 2:AopFailureTest

测试 3:AspectOrderTest


十四、Spring 5 vs Spring 6 核心差异总结

14.1 通知执行顺序变化

特性 Spring 5 (Boot 2.x) Spring 6 (Boot 3.x)
@Around-after 位置 在 @AfterReturning 之前 在所有通知之后(最后)
完整顺序 Around-before → Before → 目标方法 → Around-after → AfterReturning → After Around-before → Before → 目标方法 → AfterReturning → After → Around-after
设计理念 Around 的后置部分包裹 AfterReturning Around 真正环绕整个连接点(包括所有其他通知)
兼容性 - 更符合 AspectJ 标准语义

为什么 Spring 6 要调整?

  • @Around 真正"环绕"整个 Join Point,包括其他所有通知
  • 更符合 AspectJ 的标准语义
  • @Around 可以观察到完整的执行过程(包括其他通知的执行)

14.2 默认代理策略变化

特性 Spring Boot 2.x (Spring 5) Spring Boot 3.x (Spring 6)
默认策略 智能选择: - 有接口 → JDK 动态代理 - 无接口 → CGLIB 强制 CGLIB (即使有接口)
配置项默认值 proxy-target-class: false proxy-target-class: true
CGLIB 类名格式 $$EnhancerBySpringCGLIB$$ $$SpringCGLIB$$
如何启用 JDK 代理 默认即可(有接口时) 需显式配置: spring.aop.proxy-target-class=false

为什么 Spring Boot 3 要改?

    1. 性能考虑:CGLIB 在现代 JVM 上性能已经优于或等同于 JDK 动态代理
    1. 简化行为:统一使用一种代理方式,减少不确定性
    1. 功能一致:CGLIB 可以代理所有类,而 JDK 只能代理接口
    1. Spring Framework 6 的推动:Spring 6 本身也倾向于推荐使用 CGLIB

14.3 迁移指南

如果你从 Spring Boot 2.x 升级到 3.x,需要注意:

需要调整的地方
    1. 测试代码中的代理类名检查

    // Spring Boot 2.x assertTrue(className.contains( "$$EnhancerBySpringCGLIB" )); // Spring Boot 3.x assertTrue(className.contains( "$$SpringCGLIB" ));

    1. 通知执行顺序的断言

    // Spring Boot 2.x assertEquals( "@Around-after" , order.get( 2 )); assertEquals( "@AfterReturning" , order.get( 3 )); // Spring Boot 3.x assertEquals( "@AfterReturning" , order.get( 2 )); assertEquals( "@Around-after" , order.get( 4 )); // 移到最后

    1. 如果需要 JDK 动态代理

    application.yml spring: aop: proxy-target-class: false # 覆盖 Spring Boot 3 的默认行为

不需要调整的地方
  • AOP 的基本用法(注解、切点表达式等)完全兼容
  • 拦截器链的执行机制没有变化
  • AOP 失效场景(同类调用、final、static 等)依然相同

14.4 版本选择建议

场景 推荐版本 原因
新项目 Spring Boot 3.x + Spring 6 最新稳定版,长期支持
已有项目升级 评估影响后升级 注意通知顺序和代理策略变化
需要 JDK 代理 两者均可 2.x 默认支持,3.x 需配置
追求稳定性 Spring Boot 2.7.x 最后一个 2.x 版本,LTS

十五、总结

核心要点速查

要点 说明
五种通知 @Before、@AfterReturning、@AfterThrowing、@After、@Around
两种代理 JDK 动态代理(接口)、CGLIB(继承子类)
切点表达式 execution、within、@annotation、@within、args
拦截器链 责任链模式,proceed() 驱动
常见失效 同类调用、非 public、静态方法、final 方法
排序机制 @Order 值越小优先级越高
exposeProxy AopContext.ThreadLocal 存储当前代理

最佳实践清单

  • 优先使用 @Around 处理复杂逻辑(一个顶五个)
  • 自定义注解 + @Around 是最佳拍档
  • 同类调用优先拆分 Service,避免 self 引用
  • 切面中避免做重量级操作(每个方法都会执行)
  • 使用 !within(*..*Test*) 排除测试类
  • 了解 AOP 失效场景,避免踩坑

十六、面试自测

👉 Spring AOP 原理全解析(面试自测与答案)

基础知识

  • 能说出 JDK 动态代理和 CGLIB 的区别
  • 能解释 Proxy.newProxyInstance 的内部流程
  • 能说明 InvocationHandler.invoke 如何将调用分发给目标方法
  • 能解释 MethodProxy.invokeSuper 和 invoke 的区别
  • 能画出拦截器链(责任链)的执行流程
  • 能解释为什么 @Around 必须调用 pjp.proceed()
  • 能说明 AnnotationAwareAspectJAutoProxyCreator 的工作流程
  • 能解释 AopContext 的 ThreadLocal 机制
  • 能说出自调用失效的原因和至少两种解决方案
  • 能解释 final/static/private 方法为什么不能被代理
  • 能说清 @Order 控制切面优先级的原理
  • 能区分 Spring AOP 和 AspectJ 的能力边界
  • 能解释为什么 @Around 通常比 @Before 更快
  • 能说清切面排序的多因素规则(@Order、Ordered、@Priority)

Spring 5 vs Spring 6 差异专项

  • 能说出 Spring 5 和 Spring 6 的通知执行顺序差异
  • 能解释为什么 Spring 6 要调整 @Around-after 的位置
  • 能说清 Spring Boot 2.x 和 3.x 的默认代理策略区别
  • 知道如何在 Spring Boot 3.x 中启用 JDK 动态代理
  • 了解 CGLIB 类名格式在两个版本中的变化

十七、参考资料

Spring 官方文档

  • Spring 5 文档:AspectJ Support (5.x)
  • Spring 6 文档:AspectJ Support (6.x)
  • Spring Boot 2.x:AOP Features
  • Spring Boot 3.x:AOP Features

Spring 源码

  • org.springframework.aop.framework.ProxyFactory
  • org.springframework.aop.framework.AbstractAutoProxyCreator
  • org.springframework.aop.framework.ReflectiveMethodInvocation
  • org.springframework.aop.framework.CglibAopProxy
  • org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
  • org.springframework.aop.framework.AopContext

JDK & CGLIB 源码

  • JDK 源码:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler
  • CGLIB 源码:net.sf.cglib.proxy.Enhancernet.sf.cglib.proxy.MethodProxy

书籍

  • 《Spring 实战》第 5 章
  • 《Spring 源码深度解析》第 8 章 - AOP
  • 《Spring Boot 实战》- 版本差异章节

迁移指南

  • Spring Boot 3.0 Migration Guide
  • Spring Framework 6.0 Migration Guide
相关推荐
XiYang-DING10 小时前
Spring Boot 集成 Hutool 实现图片验证码
java·spring boot·后端
Controller-Inversion10 小时前
76. 最小覆盖子串
java·算法·leetcode
Yunzenn10 小时前
深度解析字节前沿研究-Cola DLM第 04 章:Cola DLM 架构全景 —— 三层解耦的设计哲学
java·linux·python·深度学习·面试·github·transformer
Gopher_HBo10 小时前
JVM垃圾收集算法和垃圾收集器
后端
MepSUxjvy10 小时前
拆解 OpenHands(11)--- Runtime主要组件
java·windows·microsoft
IT_陈寒10 小时前
SpringBoot自动配置偷偷给我埋了个坑
前端·人工智能·后端
ch.ju10 小时前
Java Programming Chapter 4——Member method
java·开发语言
笨蛋不要掉眼泪10 小时前
Java并发编程:ReentrantLock与AQS原理剖析
java·开发语言·并发
心.c10 小时前
CommonJS和ES Module
javascript·后端·node.js