15 面试官:Spring AOP 用 JDK 代理还是 CGLIB?你答对了但没说为什么
摘要本文深入剖析 Spring AOP 的代理机制从 JDK 动态代理与 CGLIB 的本质差异、Spring 的代理选择逻辑、AOP 通知链执行顺序到常见代理失效场景与排查方法。文章不仅对比了两种代理方式的实现原理与性能考量还结合源码解读了 Spring 的代理优先级规则并提供了面试标准回答与实战避坑指南帮助读者彻底理解 AOP 代理的底层行为与最佳实践。面试问 Spring AOP十个候选人九个上来就是JDK 动态代理和 CGLIB。你说出这两个名词我只能给你对勾不会给加分。三档答案的差距第一档说默认 JDK 动态代理没接口用 CGLIB——背的追问怎么强制切 CGLIB就卡壳。第二档说JDK 基于接口CGLIB 基于继承Spring Boot 2.0 后默认 CGLIB——有信息量了但还是没回答核心问题。第三档是从DefaultAopProxyFactory源码一行行说出代理优先级规则并能解释底层字节码行为差异的人。面试官心里其实在等三样东西你知不知道代理的本质是运行时创建字节码子类、你知不知道它为什么有时不生效、你知不知道 Spring 在什么场景下会选哪个代理方式并给出理由。这三样答全了这道题你就拿满分了。代理模式本质偷梁换柱你Autowired注入的UserService早就不是你自己写的那个UserServiceImpl了——是 Spring 帮你包的一层代理对象。代理对象在调真正方法之前插入了切面逻辑日志、权限、事务。你从头到尾没感知到代理的存在——这就是 AOP。具体是怎么包的Spring 容器启动时AbstractAutoProxyCreator一个BeanPostProcessor在 Bean 初始化的postProcessAfterInitialization阶段检查这个 Bean 是否需要代理——遍历所有 Advisor用 Pointcut 匹配 Bean 的方法如果命中就创建代理对象替代原始 Bean。这个替换过程是透明的你拿到的引用已经是代理了。问题来了这层代理到底怎么包出来的JDK 动态代理必须有接口——以及它到底生成了什么UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) - { System.out.println(前置增强); return method.invoke(target, args); } );Proxy.newProxyInstance()在运行时动态生成一个实现了指定接口的字节码类。核心限制只能代理接口中定义的方法如果目标类有 public 方法但不在接口里代理对象无法调用。且代理对象的类型是实现类接口的匿名类不能强转为具体实现类——UserService proxy可以UserServiceImpl proxy炸。但面试官如果再追问一句生成的代理类长什么样——大部分人答不上来。JDK 动态代理生成的类名通常是$Proxy0、$Proxy1这种格式它的父类是java.lang.reflect.Proxy不是你的目标类实现你指定的所有接口。每个方法被调用时实际走到InvocationHandler.invoke()由你决定要不要调真实对象。你可以通过设置系统属性sun.misc.ProxyGenerator.saveGeneratedFilestrue把生成的字节码 dump 出来看看——所有方法都转发给一个InvocationHandler而你的真实对象就存在这个 handler 里。而且有一个很多人不知道的细节如果你同时实现了多个接口JDK 代理只代理你通过Autowired声明的那个接口类型的方法。比如UserService和Auditable两个接口Autowired UserService proxy只能调UserService的方法调Auditable的方法要用((Auditable) proxy)强转。但注意——强转的前提是生成的代理类确实实现了Auditable接口。而Proxy.newProxyInstance的第二个参数是你传进去的接口数组你传了UserService.class就只生成实现了UserService的代理类强转Auditable会炸。必须把两个接口都传进去。这在实际开发中经常踩坑——你怎么知道 Spring 帮你传了哪些接口答案是 Spring 通过ClassUtils.getAllInterfaces()获取目标类的全部接口包括父接口所以只要接口继承关系写对了多个接口都能代理。CGLIB靠继承——以及 FastClass 是什么鬼Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserServiceImpl.class); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) - { System.out.println(前置增强); return proxy.invokeSuper(obj, args); });底层用 ASM 框架生成目标类的子类字节码。final 方法和 final 类无法代理——CGLIB 子类拿不到父类的 final 方法切面会静默跳过不报错排查巨坑。CGLIB 的invokeSuper走 FastClass 索引而非反射调用性能理论上比 JDK 代理更高。说理论上是因为 JDK 代理在 JVM 层面的优化非常激进。JDK 代理的InvocationHandler.invoke()调用在 JIT 编译后可以被内联——method.invoke()在热点代码路径上 JVM 会做偏向锁消除和反射调用优化。CGLIB 的 FastClass 本质是为每个方法生成一个索引调用时通过索引直接路由到目标方法避免了反射的Method.invoke()开销——但它需要多维护一套索引类。CGLIB 会为目标类和代理类各生成一个 FastClass目标类的 FastClass 负责根据方法签名找到索引代理类的 FastClass 负责根据索引找到对应的拦截器链。不过说真的在 JDK 8 的 JIT 优化面前两者的性能差距微乎其微。选 JDK 代理还是 CGLIB性能不应该成为首要理由——架构合理性才是。CGLIB 还有一个 callback filter 机制CallbackFilter你可以为不同的方法指定不同的拦截器。比如save()走事务代理find()走缓存代理——一个代理对象背后挂多个 CGLIB Callback通过accept()方法返回的索引来决定走哪个。Spring 的CglibAopProxy利用这个机制实现了如果一个方法不需要代理就跳过拦截器直接调用原方法的优化。Spring 的代理选择逻辑——源码一行行拆DefaultAopProxyFactory.createAopProxy()的规则非常简洁——先判断是不是接口if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); // 接口→JDK } return new ObjenesisCglibAopProxy(config); // 否则→CGLIBSpring Boot 2.0 起spring.aop.proxy-target-classtrue默认倾向于强制走 CGLIB。但如果类实现了接口且声明的是接口类型而非实现类Spring 仍会优先 JDK 代理。这个优先级规则看似简单实际开发中很多人搞混——你Autowired UserService接口和Autowired UserServiceImpl实现类注入的代理对象类型可能完全不同。深挖一层为什么 Spring 用 Objenesis 而不是简单的new因为 CGLIB 生成的子类不一定有无参构造器。目标类可能只有一个有参构造器传统的constructor.newInstance()会炸。Objenesis 绕过了构造器直接创建对象实例——它构造出的代理对象构造器从未被调用过。这也意味着如果你在构造器里放初始化逻辑代理对象不会执行它——又是一个静默坑。AOP 通知链的执行顺序——环绕通知到底包了什么面试官偶尔会问五个通知类型的执行顺序是什么这不是在考你记忆力是在考你理不理解 AOP 的本质是栈式嵌套调用。执行顺序是Around前置部分→Before→ 目标方法 →Around后置部分→After→AfterReturning正常返回或AfterThrowing抛异常。关键点After无论目标方法正常执行还是抛异常都会执行类似于 try-catch-finally 中的 finally 块。AfterReturning只在正常返回时执行AfterThrowing只在抛异常时执行。另外多个切面时的排序靠Order注解或实现Ordered接口。数字越小优先级越高——Order(1)的切面在最外层Order(10)的切面在最内层。这和同心圆一样最外层最先执行前置、最后执行后置。面试官可能追问的坑如果AfterThrowing里又抛了异常会怎样它会覆盖原始异常代理返回给调用方的就是你AfterThrowing里抛出的异常原始异常被吞了——排查线上 Bug 时经常以为代理逻辑没问题结果异常栈被替换了。选错会出什么事故事故一JDK 代理对象强转为实现类→ClassCastException。Autowired UserService接口类型OKAutowired UserServiceImpl实现类炸。事故二CGLIB 代理的类有final方法→切面不生效静默失败排查半天。见过最坑的线上 BugTransactional不生效三小时后发现注解加在private方法上——CGLIB 子类拿不到父类 private 方法切面直接跳过。事故三同一个 Bean 里方法自调用this.methodB()→不走代理切面失效。需要用AopContext.currentProxy()或拆 Bean 解决。事故四这个见过更惨的构造器注入的循环依赖。A 需要 BB 需要 A——构造器注入时双方都需要对方完成初始化才能构造自己直接死锁。换成Lazy或 setter 注入才能绕开。而一旦涉及代理对象的循环依赖场景更复杂——因为代理对象的创建时序会影响循环依赖的解链。Spring 的三级缓存singletonFactories就是为这个设计的但如果你不知道代理对象在三级缓存中的地位出了循环依赖的 Bug 你只能抓瞎。Spring AOP vs AspectJ——面试官在等你说出它俩不是一个东西很多人面试时说Spring AOP 就是 AspectJ——这是典型的混淆。Spring AOP 是基于代理的运行时 AOP只能在 Spring 容器管理的 Bean 上生效。AspectJ 是编译期织入通过ajc编译器或类加载期织入把切面逻辑直接写入字节码。最直观的区别Spring AOP 只能代理 Spring Bean 的 public 方法AspectJ 可以切入任何类包括非 Spring 管理的的任意方法包括 private。为什么Spring AOP 靠代理对象拦截方法调用代理只能覆盖到代理对象上的方法调用AspectJ 直接修改了字节码无差别织入。你如果想让Transactional在同一个类的内部调用中也生效Spring AOP 做不到必须拆 Bean 或用 AspectJ 的 LTWLoad-Time Weaving。 面试标准回答Spring AOP 通过 DefaultAopProxyFactory 自动选择代理方式。目标类实现了接口且声明为接口类型→JDK 动态代理Proxy.newProxyInstance运行时生成接口实现类字节码只能代理接口方法。否则→CGLIB用 ASM 生成目标类的子类字节码基于继承不能代理 final 方法或类。Spring Boot 2.0 默认 proxy-target-classtrue 倾向 CGLIB。关键差异JDK 基于接口不能强转实现类CGLIB 基于继承不能代理 final 且内部自调用不走代理。CGLIB 的 invokeSuper 走 FastClass 索引替代反射性能更高。实际开发中最容易踩的坑private 方法上 Transactional 静默失效因为 CGLIB 子类拿不到父类私有方法。通知执行顺序是 Around→Before→目标方法→Around后置→After→AfterReturning/AfterThrowing多切面通过 Order 控制优先级。Spring AOP 是运行时代理 AOP只对 Spring 管理的 Bean 的 public 方法生效AspectJ 是编译期织入可以织入任意类的任意方法。AOP 失效排查——面试官最常现场出的题面试到最后面试官可能会现场给你一段代码让你挑 Bug。这类题的核心考点就一个你知不知道什么情况下代理不生效。最常见的套路代码Service public class OrderService { Transactional public void createOrder(Order order) { // 写订单 this.sendNotification(order); // 这里的 Transactional 不会生效 } Transactional(propagation Propagation.REQUIRES_NEW) public void sendNotification(Order order) { // 发通知 } }createOrder()里的this.sendNotification()不走代理——因为this是OrderService本身不是代理对象。你调this.sendNotification()相当于直接调原始对象的sendNotification()CGLIB 那层拦截器根本没机会运行。REQUIRES_NEW也不会生效——两个方法会在同一个事务里执行。解决方式有三种一是拆成两个 Bean用Autowired注入自己二是用AopContext.currentProxy()获取当前代理对象后再调三是在配置类上加EnableAspectJAutoProxy(exposeProxy true)然后((OrderService) AopContext.currentProxy()).sendNotification(order)。面试官如果再追问AopContext.currentProxy()的原理是什么——它是通过 ThreadLocal 把当前代理对象绑定到线程上在代理方法执行前设置、执行后清除。所以它只能在代理对象的某个方法正在执行的过程中使用脱离了代理方法的调用链拿到的是 null。另外补充两个排查 AOP 失效的捷径第一启动时打 debug 日志看AbstractAutoProxyCreator的 advisor 匹配情况——Spring 会在日志里告诉你某个 Bean 被代理了还是没有匹配到切入点第二打断点在CglibAopProxy的getProxy()方法直接看返回对象的类型——是原始类还是$$EnhancerBySpringCGLIB结尾的代理类一目了然。唠点键盘之外的8 年大厂 Java 老兵不讲八股文只讲面试官心里在想什么。下一篇文章我们聊一个更底层的话题——当你认为一个 Bug 是框架的问题时其实往往是你还不够了解它的底层实现。