☰
Spring AOP底层原理:JDK代理与CGLIB动态代理深度拆解
2026/10/11 6:36:25 网站建设 项目流程

Spring AOP 这几个字,在 Java 面试里出现的频率,基本和"聊聊 HashMap 底层"差不多。问题看起来就一句话,但如果你只答出"哦就是用动态代理"这半句,面试官往往会接着往下追问:JDK 代理和 CGLIB 有什么区别?Spring 是怎么选择代理方式的?同一个类里方法互相调用为什么切面不生效?被代理的类如果是 final 的会怎样?

这一串问下来,很多人就卡住了。作为一个常年用 Spring 写业务、也被问过无数次的老开发,我把 AOP 的底层实现从头到尾给你拆一遍。这篇文章不是让你背面试题,而是真的让你把这块吃透,面试时能从概念聊到源码,再从源码聊到实践中的坑。


1. AOP 到底在解决什么问题

要理解 AOP 的底层实现,首先得搞清楚 AOP 在 Spring 里解决的是什么问题。不然你只知道它"用动态代理",却说不清为什么需要动态代理,面试照样过不了。

传统面向对象编程(OOP)用类、继承、接口来组织代码,这在处理业务主逻辑时非常顺手。但有一类逻辑很特殊:日志记录、权限校验、事务管理、性能监控。这些逻辑往往要横切到很多业务方法里,你写每个业务方法都得重复调用它们,代码散落得到处都是,改一处要动几十处。这就是所谓的"横切关注点"(Cross-Cutting Concerns)。

AOP 的核心思路,就是把这类横切逻辑从业务代码里抽出来,做成独立模块,再在运行时悄悄"织入"到目标方法的前后、异常时或者返回后。业务代码保持干净,横切逻辑统一维护。

对应到 Spring AOP 里,有这几个核心概念:

  • 切面(Aspect):横切逻辑的集合,比如一个日志切面、一个事务切面。在 Spring 中通常用@Aspect注解的类表示。
  • 连接点(JoinPoint):程序执行过程中的某个点,比如方法调用、方法执行、异常抛出。Spring AOP 只支持方法执行类型的连接点。
  • 切点(Pointcut):用来匹配连接点的表达式,决定切面要作用在哪些方法上。比如execution(* com.demo.service.*.*(..))。
  • 通知(Advice):在切点匹配的方法上执行的具体逻辑,分为前置通知、后置通知、环绕通知、异常通知、最终通知。
  • 织入(Weaving):把切面逻辑应用到目标对象并生成代理对象的过程。

这里面最关键的一点是:AOP 的织入时机有几种,编译期织入(AspectJ 的 ajc 编译器)、类加载期织入(AspectJ LTW)、运行期织入。Spring AOP 采用的是运行期织入,实现手段就是动态代理——不修改目标类的字节码,而是在运行时为目标类生成一个代理对象,由代理对象拦截方法调用,在调用前后执行通知逻辑。

这个选择非常有讲究。编译期织入需要引入 AspectJ 编译器,侵入构建流程;类加载期织入需要启动时指定 javaagent,部署麻烦。而动态代理只需要在 Spring 容器启动时多生成一个对象,对业务代码零污染,绝大多数场景够用了。代价是它在性能上、在支持的方法类型上不如 AspectJ 完整,所以 Spring AOP 的场景是"适用"而不是"全包"。


2. 动态代理:AOP 的基石

动态代理这个词,你可能听过很多遍,但底层到底发生了什么,值得掰开揉碎讲清楚。Java 里实现动态代理有两条路:JDK 动态代理和 CGLIB 动态代理。Spring AOP 的底层,说到底就是这两个机制的封装和应用。

2.1 JDK 动态代理的原理与局限

JDK 动态代理是 JDK 自带的,核心类是java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。它做的事情是:在运行时动态生成一个类,这个类实现了你指定的接口,然后把所有方法调用转发给InvocationHandler的invoke方法。

看一个最简例子:

// 定义一个接口 public interface UserService { void addUser(String name); } // 目标类,实现接口 public class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("添加用户: " + name); } } // InvocationHandler 实现 public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("[JDK代理] 方法执行前: " + method.getName()); Object result = method.invoke(target, args); System.out.println("[JDK代理] 方法执行后: " + method.getName()); return result; } }

创建代理对象并调用:

UserService target = new UserServiceImpl(); InvocationHandler handler = new LogInvocationHandler(target); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); proxy.addUser("张三");

运行结果就是在方法调用前后多出了日志输出。这里有个很重要的细节:proxy对象的类型并不是UserServiceImpl,而是一个运行时生成的Proxy子类,它实现了UserService接口。所以 JDK 动态代理有一个硬性前提:目标类必须实现至少一个接口,否则无法使用 JDK 代理。

我最初写代理时踩过一个坑:拿着UserServiceImpl的 ClassLoader 去加载代理类,结果发现getInterfaces()返回空数组。所以后来凡是遇到"这个类没有接口,但我想给它加切面"的场景,就得换 CGLIB。

从 JVM 层面看,Proxy.newProxyInstance内部会在ProxyClassFactory里根据接口信息动态生成字节码,生成一个继承Proxy、实现指定接口的类。字节码的生成过程被封装在sun.misc.ProxyGenerator(新版在java.lang.reflect.ProxyGenerator)里。这个过程不会有额外的编译步骤,类在运行时被加载进 JVM,所以叫"动态"。

2.2 CGLIB 代理的原理与适用场景

CGLIB(Code Generation Library)是一个字节码生成库,它不是 JDK 自带的,Spring 把它作为依赖引入。CGLIB 的原理和 JDK 代理完全不同:它不是通过接口,而是直接生成目标类的子类,在子类中重写目标方法,在重写的方法里插入拦截逻辑。

写一个底层示例:

// 不需要接口 public class OrderService { public void createOrder(String orderNo) { System.out.println("创建订单: " + orderNo); } } // MethodInterceptor 实现 public class OrderMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("[CGLIB] 方法执行前: " + method.getName()); Object result = proxy.invokeSuper(obj, args); System.out.println("[CGLIB] 方法执行后: " + method.getName()); return result; } }

创建代理:

Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OrderService.class); enhancer.setCallback(new OrderMethodInterceptor()); OrderService proxy = (OrderService) enhancer.create(); proxy.createOrder("NO2024001");

CGLIB 的关键机制是Enhancer创建一个目标类的子类,intercept方法里通过MethodProxy.invokeSuper调用父类的原始方法。注意,OrderService必须能被继承:它不能是 final 的,方法也不能是 final 的。否则 CGLIB 无法重写,代理逻辑就静默失效。

这里还有一个性能上的细节。CGLIB 生成的代理类在第一次调用时会建立方法索引缓存,MethodProxy.invokeSuper的调用速度在多数场景下比 JDK 代理的method.invoke更快,因为Method.invoke要做权限检查和参数装箱,而invokeSuper直接走生成的 fast class 调用。所以早期很多性能对比里,CGLIB 的表现优于 JDK 代理。但 JDK 新版本对反射做了大量优化,包括MethodHandle、方法内联等,现在两者的差距已经很小,选型时性能不该是第一考量。

另外补充一句,Enhancer.create()有一个需要注意的重载:不传参数的create()使用的是目标类的无参构造函数。如果目标类没有无参构造,会直接报错。Spring 在创建 CGLIB 代理时借助了objenesis(一个不调用构造函数创建对象的库)来绕过这个问题,但如果你自己手写 CGLIB 代理,这个坑是绕不过去的。


3. Spring AOP 源码级实现拆解

概念和底层工具讲了,现在进入重头戏:Spring AOP 到底是怎么把动态代理、切点、通知组装到一起的。面试问到这一步,如果你能把 Bean 实例化后"被代理"的流程讲通,基本就能和大部分人拉开差距。

3.1 代理对象的创建流程

Spring AOP 的入口不是一个简单的工具类,而是内嵌在 Bean 生命周期里的。Spring 容器在初始化每个 Bean 时,最后一步会检查这个 Bean 是否需要被 AOP 代理。负责这件事的核心类叫AnnotationAwareAspectJAutoProxyCreator,它是个BeanPostProcessor。

BeanPostProcessor 的postProcessAfterInitialization方法在 Bean 初始化完成后被调用,Spring 在这里调用了AbstractAutoProxyCreator.wrapIfNecessary,名字起得很直白:如果需要,就包装一层。

wrapIfNecessary的流程大致如下:

  1. 检查当前 Bean 是否已经走过了代理流程(避免循环处理)。
  2. 获取所有可用的 Advisor(通知器)。在注解驱动模式下,BeanFactoryAdvisorRetrievalHelper会从容器中找出所有类型为Advisor的 Bean,还会解析所有标注了@Aspect注解的切面类,把它们内部的通知方法包装成InstantiationModelAwarePointcutAdvisor。
  3. 遍历这些 Advisor,用切点表达式去匹配当前 Bean 的类和方法。匹配逻辑在AopUtils.findAdvisorsThatCanApply里,它会把切点解析出的方法和候选 Bean 的方法逐一比对。
  4. 找到匹配的 Advisor 后,调用createProxy生成代理对象。
  5. 生成的代理对象替换掉原来的 Bean,放入容器中。之后从容器里拿到的 Bean,其实是代理对象,不再是原对象。

这里有个容易忽略的点:wrapIfNecessary不仅作用于最终的业务 Bean,BeanPostProcessor自己也可能会被代理流程处理,所以 Spring 专门做了判断,内部基础设施 Bean 是不参与代理的。这也是为什么你断点进去看时会发现很多内部 Bean 直接走了 return。

3.2 Advisor 与 Advice 的关系

源码里经常能看到Advisor,面试时也常被问 "Advisor 和 Advice 有什么区别"。这两者的关系用一句话概括:Advice 是"干什么",Advisor 是"在哪干"。

  • Advice表示通知的具体逻辑,比如前置通知、环绕通知的拦截器实现。
  • Advisor把Pointcut(切点)和Advice组合在一起,表示"这个通知应用在哪些方法上"。

Spring 内部的InstantiationModelAspectJExpressionPointcutAdvisor就是典型的 Advisor 实现。它内部持有一个AspectJExpressionPointcut和 一个Advice。当切点匹配某个方法时,就把对应的 Advice 加入该方法的拦截器链。

所以整个 AOP 配置的解析链是:

@Aspect 类 └─ @Before/@Around/@After 等方法,被解析为一个个 Advice └─ 加上 Pointcut 表达式,包装成 Advisor └─ 由 AspectJAutoProxyCreator 在 Bean 初始化后消费

面试时能说出这层关系,比单纯背概念要加分不少。因为大多数人只背了 Advice 的几种类型,却说不清 Spring 内部为什么要引入 Advisor 这个中间层——就是因为通知必须和切点捆绑才有意义。

3.3 拦截器链的调用过程

代理对象创建出来后,方法调用会走一条链路。这条链路的实现是 Spring AOP 最精华的部分。

先看createProxy内部,DefaultAopProxyFactory.createAopProxy里有这样一段核心判断逻辑(简化描述):

public AopProxy createAopProxy(AdvisedSupport config) { 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); } else { return new JdkDynamicAopProxy(config); } }

一句话总结:如果目标类是接口,或者本身已经是代理类,就用 JDK 动态代理;否则优先用 CGLIB 代理。注意这里的"优先"体现在proxyTargetClass=true时,Spring 强制走 CGLIB。

JdkDynamicAopProxy内部实现了InvocationHandler,它的invoke方法会构建拦截器链;CglibAopProxy则实现了MethodInterceptor接口,由 CGLIB 在拦截时调用。两者的后续处理殊途同归,都会进入ReflectiveMethodInvocation.proceed()。

ReflectiveMethodInvocation.proceed()是整条链路的主循环。它维护一个当前拦截器的索引currentInterceptorIndex,初始为 -1。proceed每次执行时:

  1. 判断索引是否到了拦截器链末尾。
  2. 如果到了末尾,就调用真正目标方法的invokeJoinpoint。
  3. 如果还有拦截器,就取下一个拦截器,调用它的invoke方法,并把currentInterceptorIndex加一。
  4. 每个拦截器在业务逻辑执行前后可以插入自己的逻辑,然后手动调用proceed()继续推进。

这就是经典的责任链模式。业务代码本身不知道外面包了几层通知,它只是被逐层传递调用。我们平时写环绕通知时,必须调用pjp.proceed()才能让目标方法执行,原因就在这里——ProceedingJoinPoint.proceed()本质上就是在推进这条拦截器链。

关于拦截器链的顺序,Spring 内部定义了一套排序规则。比如前置通知、环绕通知、返回通知、异常通知、最终通知的执行顺序,在@Around和@Before同时存在时,先执行环绕通知的前半段,再执行前置通知,然后是目标方法,接着是返回通知,最后是环绕通知的后半段。这个顺序如果你在业务里依赖了,一定得清楚:环绕通知不是完全"包住"所有前置逻辑的,即使没有环绕通知,前置通知照样执行。


4. 底层机制之外的高频追问

很多面试官问完"底层实现",紧接着就会抛出几个衍生的场景题。这些题表面上是考用法,实际上还是考底层理解。

4.1 Spring 如何决定用 JDK 代理还是 CGLIB

前面源码已经给出了答案。但在不同 Spring 版本里,这个默认行为是有变化的,面试里容易被挖。

  • 在 Spring Boot 2.x 之前,Spring AOP 的默认策略是:目标类实现了接口就用 JDK 动态代理,没有接口才退化为 CGLIB。
  • Spring Boot 2.x 开始,spring.aop.proxy-target-class默认值为true,意味着默认强制使用 CGLIB 代理,即使目标类实现了接口。

当初这个改动引起了不小的讨论。CGLIB 代理有自己的短板,比如不能代理 final 类、不能代理 final 方法,对远程调用框架(面向接口的 RPC 客户端)不太友好。强制 CGLIB 的官方理由是:CGLIB 代理不会因为目标类没有接口而报错,行为更一致,而且避免了ClassCastException(JDK 代理对象转换到目标类类型失败)。实际开发里,如果你用@Autowired注入一个具体实现类而不是接口类型,JDK 代理很容易出问题——代理对象是接口类型的子类,不是实现类的子类,注入到具体类里会抛类型转换异常。

所以现在的局面是:主流 Spring Boot 项目里你拿到的 Bean 默认都是 CGLIB 代理。除非你手写SpringApplication配置时把它显式改回 JDK 代理。

4.2 同一个类里的方法调用,为什么切面不生效

这是实践中最常见、最经典的一个问题。代码如下:

@Service public class BillService { @Transactional public void pay(String orderId) { // 业务逻辑 this.afterPay(orderId); } @Transactional public void afterPay(String orderId) { // 后续逻辑 } }

你期望afterPay也被事务包裹,结果发现只有pay有事务,afterPay根本没开事务。原因很简单:Spring 生成的是代理对象,容器注入给你的BillService其实是代理。外部调用billService.pay()时,调用的是代理对象上的方法,代理会拦截、开启事务、调用目标方法、提交/回滚事务。

但pay方法里的this.afterPay(orderId),这个this指向的是目标对象本身,不是代理对象。既然不是代理对象,也就没有任何拦截器参与,自然就没有事务。

解决这个问题有三个思路,面试时都值得提:

  1. 拆开类,把afterPay放到另一个被 Spring 管理的 Bean 里,注入后通过代理对象调用。
  2. 自己注入自己,比如在BillService里注入BillService的代理(Spring 允许这样循环依赖,但不推荐)。
  3. 使用AopContext.currentProxy(),前提是开启@EnableAspectJAutoProxy(exposeProxy = true),然后((BillService) AopContext.currentProxy()).afterPay(orderId)。

我自己在实际项目里,最常用第一种。第三种虽然直接,但AopContext是基于 ThreadLocal 的,在大规模异步转发场景下要特别小心,稍不注意就会取错代理。

4.3 为什么 private 方法和 final 方法不能被 AOP

JDK 动态代理只能代理接口方法,private方法本来就不在代理范围。CGLIB 的情况类似,它靠生成子类重写方法,private方法不可继承不可重写,final方法不可重写,static方法也不是实例方法。所以 Spring AOP 对这三类方法全部无能为力,连报错都不会报,静默跳过。

这个问题在面试里经常被包装成:"@Transactional加在 private 方法上会怎样?"正确答案是:不会生效,也不报错。很多人以为 Spring 会提示,其实完全没提示,事务配置静默失效,数据回滚不了,线上出事就是这么来的。

我见过一个真实的生产事故:有人在工具类里给一个static方法加了@Transactional,结果发现数据异常时没有回滚。排查了很久才发现代理根本无法拦截static方法。@Transactional的正确用法是:加在非 private 的实例方法上,并且通过代理对象调用。


5. 手写一个极简 AOP 框架

光看不练,底层机制还是容易忘。我建议你亲自动手写一个最简的 AOP,不用 Spring,只用 JDK 动态代理写。这个过程比背十篇源码分析都管用。

这里我给出一个非常精简的实现思路。首先定义几个核心接口:

// 切点:判断某个方法是否需要被增强 public interface Pointcut { boolean matches(Class<?> targetClass, Method method); } // 通知:封装增强逻辑 public interface Advice { Object invoke(JoinPoint joinPoint) throws Throwable; } // 连接点:包含目标对象、目标方法、参数 public class JoinPoint { private final Object target; private final Method method; private final Object[] args; // 构造器、getter 省略 public Object proceed() throws Throwable { return method.invoke(target, args); } } // 代理工厂 public class SimpleAopProxyFactory { private final Pointcut pointcut; private final Advice advice; public SimpleAopProxyFactory(Pointcut pointcut, Advice advice) { this.pointcut = pointcut; this.advice = advice; } @SuppressWarnings("unchecked") public <T> T createProxy(T target) { Class<?> targetClass = target.getClass(); // 只演示 JDK 代理路径,要求目标类有接口 return (T) Proxy.newProxyInstance( targetClass.getClassLoader(), targetClass.getInterfaces(), (proxy, method, args) -> { if (pointcut.matches(targetClass, method)) { return advice.invoke(new JoinPoint(target, method, args)); } return method.invoke(target, args); } ); } }

然后写个测试:

public interface GreetingService { void sayHello(String name); } public class GreetingServiceImpl implements GreetingService { @Override public void sayHello(String name) { System.out.println("Hello, " + name); } } // 使用 Pointcut cut = (clazz, method) -> method.getName().startsWith("say"); Advice advice = joinPoint -> { System.out.println("before..."); Object result = joinPoint.proceed(); System.out.println("after..."); return result; }; GreetingService service = new SimpleAopProxyFactory(cut, advice) .createProxy(new GreetingServiceImpl()); service.sayHello("World");

这个框架当然非常简陋,但它把 AOP 的核心抽出来了:切点决定"哪些方法要管",通知决定"管的时候干什么",代理决定"怎么把管理插进去"。Spring 的底层实现就是这三件事在更复杂的封装下的组合,只是它的切点支持表达式解析、通知有五种类型、代理支持 JDK 和 CGLIB 两种、拦截器链支持多层嵌套。

你要是在自己的项目里再给它加上 CGLIB 支持,加上多级拦截器链,就非常接近 Spring 的实现思路了。自己亲手写一遍的责任链proceed(),比任何教程都记得牢。


6. 实战中的坑与排查技巧

最后把我在实际项目中踩过、帮别人排查过的问题汇总一下。这些内容平常文档里不太会写,面试时讲出来也显得有真实经验。

6.1 常见错误速查

问题现象根因处理方式
代理对象无法注入到具体类型启动报ClassCastException默认 JDK 代理,注入目标使用了实现类类型改为注入接口,或开启proxyTargetClass=true
自调用切面不生效日志/事务表现异常this调用不走代理拆类或使用AopContext.currentProxy()
@Transactional加在 private 方法上事务不回滚代理无法拦截 private 方法改为非 private,并通过代理调用
final 类/方法事务失效无任何报错CGLIB 无法继承 final 类、重写 final 方法去掉 final,或放弃对该方法的代理
CGLIB 代理创建失败报无法实例化代理类目标类没有无参构造补充无参构造,或使用 objenesis 方案
@Transactional在static方法上完全无事务效果静态方法不参与实例代理去掉 static,注入实例调用

6.2 定位代理问题的小技巧

遇到 AOP 相关的问题,第一件事是确认 Bean 到底存的是不是代理对象。两种最快的方式:

  1. 在方法里打印this.getClass(),如果输出是xxx$$EnhancerBySpringCGLIB$$...或者com.sun.proxy.$Proxy...,说明这个对象是代理,问题出在后续调用路径。
  2. 在启动日志或者调试时,查目标 Bean 对应的BeanDefinition的attribute,看看它的代理类型标记。

判断一个类能不能被 CGLIB 代理,有个高效的检查方式:直接看Enhancer是否抛异常,或者在编译时注意final关键字。不要试图在运行时去猜,很多编译器不提示这些问题。

还有一个容易被忽视的场景:代理对象的toString()、equals()等方法也会被拦截吗?答案是 Spring 做了处理。CglibAopProxy和JdkDynamicAopProxy在拦截时都对equals、hashCode、toString进了特殊处理,直接转发给目标对象而不是走通知逻辑,否则日志切面会把所有 Bean 的toString都打一遍,系统日志直接爆炸。

6.3 性能预期要摆正

经常有人问:用了 AOP,性能是不是会下降很明显?如果是高频调用的小方法,代理带来的反射开销确实存在。但现代 JVM 对反射有优化,CGLIB 走的 fast class 调用也很高效,多数业务场景下这点损耗微乎其微。

真正要注意的不是代理开销,而是切面逻辑本身的性能。如果你的前置通知里做了数据库查询、调了外部接口、写了大量日志,那不管有没有代理,这方法都快不了。很多性能问题被甩锅给 AOP,本质上是因为切面里塞了不该塞的重量级操作。

另外,Spring 对拦截器链是有缓存的。AdvisedSupport里缓存了advisors和methodCache,同一个代理对象的方法第一次调用时构建好拦截器链,后续直接复用,不会每次重新解析切点表达式。这也是@AspectJ切面在启动时比较慢、运行起来却很快的原因。


写这块内容的过程里,我又把ReflectiveMethodInvocation.proceed()的源码重新翻了一遍。说句实话,AOP 的底层并不复杂,复杂的是它的设计理念:把横切逻辑和业务逻辑解耦,用责任链把多种通知有序串起来,用两种动态代理策略覆盖不同场景。每次面试前,我都建议候选人别只背结论,而是亲手写一个小 AOP demo,再对着DefaultAopProxyFactory看一遍分支判断。你把这两件事做了,面试官问什么变体都难不住你。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询