云计算百科
云计算领域专业知识百科平台

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.saveGeneratedFiles=true 把生成的字节码 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); // 否则→CGLIB

Spring Boot 2.0 起 spring.aop.proxy-target-class=true(默认),倾向于强制走 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(接口类型)OK,@Autowired UserServiceImpl(实现类)炸。

事故二:CGLIB 代理的类有 final 方法→切面不生效,静默失败排查半天。见过最坑的线上 Bug:@Transactional 不生效,三小时后发现注解加在 private 方法上——CGLIB 子类拿不到父类 private 方法,切面直接跳过。

事故三:同一个 Bean 里方法自调用 this.methodB()→不走代理,切面失效。需要用 AopContext.currentProxy() 或拆 Bean 解决。

事故四(这个见过更惨的):构造器注入的循环依赖。A 需要 B,B 需要 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 的 LTW(Load-Time Weaving)。


🎯 面试标准回答

"Spring AOP 通过 DefaultAopProxyFactory 自动选择代理方式。目标类实现了接口且声明为接口类型→JDK 动态代理,Proxy.newProxyInstance 运行时生成接口实现类字节码,只能代理接口方法。否则→CGLIB,用 ASM 生成目标类的子类字节码,基于继承不能代理 final 方法或类。Spring Boot 2.0 默认 proxy-target-class=true 倾向 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 是框架的问题时,其实往往是你还不够了解它的底层实现。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 15 面试官:Spring AOP 用 JDK 代理还是 CGLIB?你答对了但没说为什么
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!