动态代理双雄:JDK Proxy 与 CGLIB 原理实战
上一节说 AOP 靠「运行期造一个替身」。这一节就是把那个替身拆开看:它到底是谁造的、长什么样、调用怎么走到你的代码里。三个词先按小白口径钉死:
- 反射(reflection)——程序在运行时反过来操作类本身的能力,比如拿着一个
Method对象调method.invoke(target, args),等价于「把方法名写在纸上,照着喊人干活」,而不是编译时就写死调用哪一行。 - 字节码(bytecode)——
.java编译后给 JVM 读的中间语言,存在.class文件里;CGLIB 之所以能「凭空造子类」,就是直接在内存里拼这段指令,不经过javac。 - 动态代理——程序跑到一半才现造一个「长得和目标一样」的类并实例化它,你写的源码里从来没有这个类。
JDK 动态代理像前台替身。你去一家公司找总经理,接待你的前台长得跟他一模一样、名片上也写着同一个职位(实现同一个接口),你说的每句话(每个方法调用)都被前台记下来转述给真正的总经理,再把答复递回给你。总经理本人毫不知情,也毫未改变——变的只是「谁来接待你」。而 CGLIB 更像认了个干儿子:直接以目标类为父类生成子类,在子类方法里插手脚,所以父类不能是 final(没爹可认)、方法不能是 final(不许重写)。
InvocationHandler 就是快递分拣中心。所有包裹(调用)不管寄给谁,先统一送到这一个地方,贴着的单据上写着「收件人 + 面单号」(Method + args);分拣中心决定是先扫码登记再派送(前置通知)、直接派送(透传)、还是退回(抛异常)。这就是为什么增强逻辑只写一遍却能覆盖全部方法。

学完这节你要能回答:
$Proxy0凭什么只能「实现接口」而不能继承我的实现类?- 什么情况下 Spring 只能用 CGLIB?final 类和 final 方法的后果有何不同?
(UserServiceImpl) proxy为什么会炸,@Transactional自调用又为什么会静默失效?
上一节我们手写过一个静态代理,当时它的「写不完」还只是隐约的痛感。把场景放大,痛感立刻变成切肤之痛:系统里有三个服务接口。
public interface UserService { User findById(Long id); void save(User user); }public interface OrderService { Order create(OrderDTO dto); void cancel(Long id); }public interface PayService { void pay(BigDecimal amount); }// 每加一个接口,就要多写一个代理类;每个方法都要手写一遍「转发 + 打日志」public class UserServiceLogProxy implements UserService { /* 方法逐个转发…… */ }public class OrderServiceLogProxy implements OrderService { /* 方法逐个转发…… */ }public class PayServiceLogProxy implements PayService { /* 方法逐个转发…… */ }- 代理类和接口是 1:1 绑定,接口数量增长,代理类线性增长
- 接口每加一个方法,对应代理类就要补一段几乎一样的转发代码
- 一旦要加第二个切面(事务、权限),代理类的数量还会按切面组合继续翻倍
问题的根子在于:「转发 + 增强」这套动作对所有接口都一样,却被我们手写了很多遍。既然它和具体接口无关,就应该交给程序在运行时自动生成——这就是「动态代理」的动机。
JDK 自带了一套动态代理机制,核心只有两个类:java.lang.reflect.Proxy 和 InvocationHandler。下面这个例子可以直接运行:
package com.example.proxy;import java.lang.reflect.*;import java.util.Arrays;public class JdkProxyDemo { public static void main(String[] args) { UserService target = new UserServiceImpl(); // 真实目标 UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 1. 用哪个类加载器加载代理类 new Class<?>[]{ UserService.class }, // 2. 代理类要实现哪些接口 new LogInvocationHandler(target) // 3. 调用转发给谁处理 ); proxy.save(new User("alice")); System.out.println("main 拿到: " + proxy.findById(1L)); }}class LogInvocationHandler implements InvocationHandler { private final Object target; LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("[前] " + method.getName() + " 参数=" + Arrays.toString(args)); Object result = method.invoke(target, args); // 反射调用真实目标 System.out.println("[后] " + method.getName() + " 返回=" + result); return result; }}注意:Proxy.newProxyInstance 必须在有接口的前提下工作。把 new Class<?>[]{ UserService.class } 换成 UserServiceImpl.class 并不改变本质——代理类仍然只会实现「接口」,不会继承实现类。如果 UserServiceImpl 没有实现任何接口,JDK 代理直接无从下手。
运行结果:
[前] save 参数=[User{name='alice'}][后] save 返回=null[前] findById 参数=[1][后] findById 返回=User{name='alice'}main 拿到: User{name='alice'}- 三个参数的含义:类加载器、代理类要实现的接口数组、负责转发调用的
InvocationHandler - 增强逻辑全部集中在
invoke一个方法里,与具体是哪个接口、哪个方法无关 method.invoke(target, args)依靠反射调用真实目标,这是「转发」的实现方式
为什么 JDK 代理必须要求目标实现接口? 因为在 Java 里一个类只能继承一个父类,而 JDK 生成的代理类已经 extends Proxy 了,没办法再继承你的实现类,于是只能通过「实现接口」来做到「看起来和目标同类型」。这条限制直接决定了它的适用边界,也就引出了下一节的 CGLIB。
JDK 运行时会在内存里生成一个代理类,名字形如 $Proxy0、$Proxy1……它大概长这样(简化示意):
// JDK 运行时生成的代理类(示意,非源码)public final class $Proxy0 extends Proxy implements UserService { private static Method m3; // 对应 save(User) public $Proxy0(InvocationHandler h) { super(h); // 把 handler 交给父类 Proxy 保存 } @Override public void save(User user) { try { // 关键:这一切都在把「方法 + 参数」原样打包,交给 handler.invoke super.h.invoke(this, m3, new Object[]{ user }); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } }}- 代理类由 JVM 在运行时生成,可以加
-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true把它落盘查看 - 它
extends Proxy,所以只能通过「实现接口」来跟目标「同类型」——这正是 Java 单继承限制的直接后果 - 每个接口方法体里几乎只有一行:把
Method和args交给h.invoke,真正的逻辑全在 handler 里

这条动图把上面那一段代码放慢成六帧:注意第 3 帧之后,你的源码才第一次被执行——在 $Proxy0 眼里,整个世界只有「打包」和「转交」两件事。
CGLIB(Code Generation Library)走的是另一条路:运行时为目标类生成一个子类,重写父类方法并插入回调。它依赖 ASM 直接操作字节码。核心是 Enhancer 和 MethodInterceptor:
package com.example.proxy;import net.sf.cglib.proxy.*;public class CglibProxyDemo { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OrderService.class); // 指定「父类」,不是接口 enhancer.setCallback(new LogMethodInterceptor()); // 方法拦截器 OrderService proxy = (OrderService) enhancer.create(); // 生成子类实例 proxy.create(new OrderDTO("A1001")); }}class LogMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { System.out.println("[前] " + method.getName()); Object result = mp.invokeSuper(obj, args); // 调用父类(真实)的方法 System.out.println("[后] " + method.getName()); return result; }}- CGLIB 生成的代理对象是目标类的子类,不需要目标实现任何接口
mp.invokeSuper(obj, args)调用的是「父类的原方法」,等价于 JDK 代理里的method.invoke(target, args)- 无需接口这一点,正好补上了 JDK 代理的短板
「凭空造一个子类」听着像魔法,这段六帧动画把它拆成了流水线:注意第 ② 帧——字节码是 ASM 在内存里拼出来的,不经过 javac,所以你在 target 目录里永远找不到这个类;第 ⑤ 帧才是真正调到父类原方法的那一下:

但它也有代价——既然是「继承」,就吃 Java 继承的全部限制。下面两条是踩坑重灾区:
public final class FinalService { // final 类:无法被继承,CGLIB 直接报错 public void hello() { }}public class NormalService { public final void fast() { } // final 方法:拦不到,通知会「静默失效」 private void inner() { } // private 方法:同样拦不到}坑:CGLIB 挑不出 final 类的毛病——它会直接抛 IllegalArgumentException: Cannot subclass final class,一下就能定位。真正难查的是 final / private / static 方法:代理能创建成功、程序能跑,但那几个方法上的通知一声不响地失效了,日志里什么都看不到。

| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现方式 | 运行时生成 $Proxy0,实现接口 | 运行时生成子类,继承目标类 |
| 前置条件 | 目标必须实现接口 | 目标类与目标方法不能是 final |
| 生成时机 | 首次调用时生成并缓存 | 首次调用时生成并缓存 |
| 性能 | JDK 8 之后大幅优化,差距已明显缩小 | 早期更快;创建代理较慢,调用较快 |
| 适用场景 | 面向接口编程、API 稳定 | 无接口、需要代理具体类 |
| 常见异常 | 强转实现类抛 ClassCastException | final 类抛 IllegalArgumentException |
一句话记忆:JDK 代理靠接口,CGLIB 靠继承。谁有接口、谁没有接口,就是选型的第一个岔路口。
Spring AOP 的选型逻辑集中在 DefaultAopProxyFactory,它会在创建代理前做一次判断。简化后的核心逻辑如下:
// DefaultAopProxyFactory.createAopProxy 的核心判断(简化)if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { // 走 CGLIB:强制 CGLIB,或目标没有可用接口 return new CglibAopProxy(config);}// 有接口、又没有强制 -> 走 JDKreturn new JdkDynamicAopProxy(config);- 默认规则:目标实现了接口 → 优先 JDK 代理;没有接口 → 只能用 CGLIB
- 强制开关:
@EnableAspectJAutoProxy(proxyTargetClass = true)或配置spring.aop.proxy-target-class=true - Spring Boot 2.x 起默认 CGLIB:
AopAutoConfiguration把proxy-target-class默认设为true,这样「按实现类注入」也不会再撞上ClassCastException
三段 if 读起来抽象,把它画成判定树就是四次追问,任何一格都能对上你自己项目里的现象:

理解这条规则很实用:当你明明配了切面却报 ClassCastException,大概率是「用实现类类型接收了 JDK 代理」。要么注入时改用接口类型,要么打开 proxyTargetClass。
「JDK 代理比 CGLIB 慢」是一个流传很久、但已经过时的结论。JDK 8 之后,Proxy 的调用路径被大幅优化,两者差距已经缩小到多数业务场景可以忽略的程度。下面是一段简化的基准代码:
@Testvoid proxyThroughput() { // 关键:先预热,让 JIT 编译生效,否则测的是解释执行 for (int i = 0; i < 200_000; i++) { jdkProxy.method(); cglibProxy.method(); } long t1 = System.nanoTime(); for (int i = 0; i < 10_000_000; i++) jdkProxy.method(); long jdk = System.nanoTime() - t1; long t2 = System.nanoTime(); for (int i = 0; i < 10_000_000; i++) cglibProxy.method(); long cglib = System.nanoTime() - t2; System.out.printf("JDK=%dms CGLIB=%dms%n", jdk / 1_000_000, cglib / 1_000_000);}在一台普通机器上的相对量级(仅用于理解趋势,具体数值随 JDK 版本与硬件变化):
| 指标 | JDK 动态代理 | CGLIB | 说明 |
|---|---|---|---|
| 创建代理 | 较慢(反射 + 字节码生成) | 更慢(ASM 生成子类) | 都只发生一次,可忽略 |
| 单次调用 | 略慢 | 略快 | JDK 8+ 后差距很小 |
| 冷启动开销 | 低 | 高 | 代理类生成更重 |
代理的创建成本发生在启动期且只发生一次,调用成本才是运行期大头。除非你在极端热点路径上做纳秒级优化,否则不必因为「性能」而纠结选型——真正该考虑的是「目标有没有接口」这个决定性因素。
把代理对象强转成实现类,只有 CGLIB 才走得通。JDK 代理生成的是 $Proxy0,它只实现了接口、不继承 UserServiceImpl,所以 (UserServiceImpl) proxy 会抛 ClassCastException。注入时统一使用接口类型(UserService),而不是实现类型(UserServiceImpl),就能从根上避开这个坑。
CGLIB 会「静默失败」在 final / private / static 方法上。它们既不会报错,也不会触发通知,只能靠你在排查时意识到「这些方法天生不可代理」。反过来,如果目标类本身被声明为 final,CGLIB 会直接创建失败——这一点反而更好定位。
下面这个演示在 JDK 与 CGLIB 之间切换,把代理类的「签名形态」和「通知链」同时展示出来。对比着看,你会更直观地感受到:一个实现接口、一个继承子类,但拦截调用的方式殊途同归。

四个实验按顺序跑。第一个是本章主菜——在 JDK 与 CGLIB 之间切换,看代理类的签名形态与通知链如何变化,error 参数还会演示目标方法抛异常时通知链怎么走:
有了替身,才谈得上织入。weave 把「同一个方法在没有切面 / 有切面 / 通知链内部 / 代价」四种状态并排给你看,正好印证第十一节 AOP 那套术语落到运行时的样子:
替身是谁给套上去的?autoptr 演示自动代理创建器在后置处理里做候选判定、再由代理工厂产出对象的全过程——把第 9 篇「AOP 代理诞生于初始化后置」这句话变成可见的时序:
最后补一个必须知道的组合题:多个切面同时命中一个方法时,洋葱的进出顺序由 @Order 决定。txaspect 专门演示 @Transactional 与自定义切面的先后陷阱:
上面这一组讲的是「几个替身叠在一起」,还没回答「单个替身里到底装了什么」。advices 把一个方法上的五种通知全挂出来,看它们如何被塞进代理的调用链:
再往前追一步时间线:这个类到底在 Bean 生命周期的哪一秒被换成替身?答案是初始化后置(postProcessAfterInitialization)——Bean 本身已经造好、属性也填完了,代理是在它「出院」前最后一道手续上套的:
前面第三节看过 $Proxy0 的源码示意,第四节的动图也讲了 CGLIB,但「一次调用究竟怎么从你手上走到 handler 里」这件事,还是得逐行走。下面是 JDK 代理那一路的单步台:连点「下一步」,盯住标着 (4) 的那一行——你的代码第一次被执行,比调用点晚了两层栈:
UserService svc = ctx.getBean(UserService.class); // svc 其实是 jdk.proxy2.$Proxy78svc.save(new User("alice")); // (1) 你写的这一行就是调用点// ---- 镜头进入 $Proxy78 ----public void save(User u) { // (2) 代理类里现生成的方法体 super.h.invoke(this, m3, new Object[]{ u }); // (3) 打包「方法号 + 参数」转交 handler// ---- 镜头进入你写的 LogInvocationHandler ----System.out.println("[前] " + method.getName()); // (4) 前置增强:从这里起才是你的代码Object r = method.invoke(target, args); // (5) 反射调用真实实现对象System.out.println("[后] " + r); // (6) 后置增强return r; // (7) 交回 $Proxy78,再回到调用方} // (8) 未声明的受检异常在这里被包成一层壳| svc 的类名 | jdk.proxy2.$Proxy78 |
| Proxy.isProxyClass | true |
| target | UserServiceImpl@1a2b3c |
JdkProxyDemo.main六种 JDK / CGLIB 特有的写法与报错,靠位置猜没用——右侧全是各不相同的事实,只能真认出来:
实验做到这里,可以换成命令行自己敲了。这台控制台连着浏览器里的同一个容器,回显全部由内核算出来——先 boot,再 beans 挨个看类名后缀,你就知道每个 Bean 家是哪条路:
「Spring 到底给我造了哪种代理」不是玄学,就是这三个条件的组合:
# DefaultAopProxyFactory: 有接口且未强制 -> JDKproxy = com.sun.proxy.$Proxy78 implements UserServiceinject by interface: OK结论:教科书里的默认组合
用法建议:先把第一格当基准,然后每次只动一个开关。你会自己发现两条铁律——① proxyTargetClass=true 一旦为真,「有没有接口」就不再影响代理类型;② 接收变量的类型必须与生成的替身兼容,这正是 ClassCastException 的全部来源。
这一节的报错有一半炸得很响,另一半一声不响——后者更难查。
先看一段最常抄错的栈:
java.lang.ClassCastException: class com.sun.proxy.$Proxy78 cannot be cast to classcom.example.service.UserServiceImpl (com.sun.proxy.$Proxy78 andcom.example.service.UserServiceImpl are in unnamed module of loader 'app') at com.example.controller.UserController.<init>(UserController.java:23)读法:前半句告诉你替身是 JDK 造的(com.sun.proxy.$Proxy78),后半句告诉你你用了实现类去接它。两者不可能兼容——$Proxy0 只继承 Proxy、只实现接口,跟你写的 UserServiceImpl 没有任何继承关系。
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
java.lang.ClassCastException: class com.sun.proxy.$Proxy78 cannot be cast to class com.example.UserServiceImpl | 用 JDK 代理(有接口、未强制 CGLIB)却按实现类类型接收/强转 | 首选把字段类型改成接口;确需按实现类注入就设 spring.aop.proxy-target-class=true(Boot 默认已如此) | 本篇第六节 |
org.springframework.cglib.core.CodeGenerationException: java.lang.IllegalArgumentException: Cannot subclass final class com.example.FinalService | 目标类是 final,CGLIB 无法生成子类 | 去掉 final;或给它抽一个接口让 Spring 走 JDK 路线;或对这一个 Bean 局部关掉代理 | 本篇第四节 |
@Transactional 加在 final 方法上:启动正常、无警告、更新却在异常后提交了 | 子类代理不能重写 final 方法,通知静默丢失 | 去掉 final;打 getClass().getName() 确认代理存在,再逐个检查方法修饰符 | 第 31 篇 |
private/static 方法上的通知完全不执行 | 私有与静态方法不是代理的拦截点(静态属于类,代理持有实例) | 改成 public 实例方法;确需静态逻辑就抽到工具类并由被代理的方法调用 | 第 11 篇第七节 |
同类里 this.save(x) 导致 @Transactional/@Cacheable 失效(无任何报错) | this 指向原始对象而非容器里的替身,调用没经过代理 | 拆到另一个 Bean;或注入自身代理;或 ((MyService) AopContext.currentProxy()).save(x) 配合 @EnableAspectJAutoProxy(exposeProxy = true) | 第 14、15 篇 |
java.lang.reflect.UndeclaredThrowableException 从代理调用处抛出 | InvocationHandler.invoke 抛出了接口方法签名上没声明的受检异常,$Proxy0 只能把它包一层 | 在 handler 里判断:非接口声明的 RuntimeException/Error 原样抛,其他明确包装;或在接口方法上补 throws | 本篇第三节 |
NoClassDefFoundError: net/sf/cglib/proxy/MethodInterceptor(手写 CGLIB demo 时) | 示例代码引的是老 cglib 包名,而 Spring 5+ 用的是内联的 org.springframework.cglib.* | 改用 org.springframework.cglib.proxy.Enhancer,或直接引 spring-boot-starter-aop | 本篇第四节 |
代理对象的 equals/hashCode/toString 行为诡异 | 这三种方法同样会被路由到 InvocationHandler,而 AOP 框架内部对它们有特殊处理 | 不要依赖 toString() 判断业务身份;比对对象请用明确的业务 ID 字段 | 第 14 篇 |
分不清自己拿到的是哪种替身时,就打一行 svc.getClass().getName():出现 $Proxy 是 JDK,出现 $$SpringCGLIB$$0 是 CGLIB,两个都没有说明压根没被代理。
本篇最经典的那句 ClassCastException,栈其实短得离谱——难的不是读栈,是看懂「$Proxy78 凭什么不是 UserServiceImpl」。点出你认为的凶手帧:
同事把 spring.aop.proxy-target-class 改成 false(或者项目从 Boot 1.x 升级上来的路上),UserController 的构造器参数写的是实现类,应用一起来就炸。
亲手造一个 JDK 动态代理,并把 JVM 现生成的 $Proxy0 源码落盘看一眼——这一步最能建立「替身是真的存在的类」的体感。完整可跑代码:
package com.example.proxy;public interface UserService { String find(Long id);}public class UserServiceImpl implements UserService { @Override public String find(Long id) { return "user-" + id; }}package com.example.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.Arrays;public class JdkProxyLab { public static void main(String[] args) throws Throwable { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class<?>[]{ UserService.class }, new InvocationHandler() { @Override public Object invoke(Object p, Method m, Object[] a) throws Throwable { System.out.println("[handler] 收到调用: " + m.getName() + " args=" + Arrays.toString(a)); Object r = m.invoke(target, a); // 反射调到真实实现 System.out.println("[handler] 返回: " + r); return r; } }); System.out.println("[class] " + proxy.getClass().getName()); System.out.println("[ifaces] " + Arrays.toString(proxy.getClass().getInterfaces())); System.out.println("[isProxy?] " + Proxy.isProxyClass(proxy.getClass())); System.out.println("[call ] " + proxy.find(7L)); // 亲手验证「不能强转实现类」这条铁律 try { UserServiceImpl bad = (UserServiceImpl) proxy; System.out.println("[bad] 竟然成功了?" + bad); } catch (ClassCastException e) { System.out.println("[bad] " + e.getMessage()); } }}预期输出:
[class] jdk.proxy2.$Proxy7[ifaces] [interface com.example.UserService][isProxy?] true[handler] 收到调用: find args=[7][handler] 返回: user-7[call ] user-7[bad] class jdk.proxy2.$Proxy7 cannot be cast to class com.example.UserServiceImpl ...再把生成的代理类存成文件看看(Java 9+ 的参数):
-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true跑完在项目根目录会出现 jdk/proxy2/$Proxy7.class,用 javap -p jdk.proxy2.$Proxy7 反编译,你能看到每个接口方法体里那句 h.invoke(this, ...) ——和本文第三节的示意代码逐行对得上。
目标:证明「final 方法上的通知静默失效」。
提示:把 UserServiceImpl.find 复制一份改成 public final String findCached(Long id),再在 handler 里打印方法名,最后通过接口调用这两个方法各一次。若你想验证 Spring 场景,就给实现类加一个 @Transactional public final void tx(),并从外部调用它。
你会观察到:非 final 的 find 每次都经过 handler 并打印;findCached 若经由接口暴露也照样会被拦(因为 JDK 代理只看接口),但在 CGLIB 场景下 tx() 一条通知都不会触发,也没有任何报错或警告——这就是为什么本节反复强调「打 getClass().getName() 再逐个查方法修饰符」。
做一个「迷你 IoC + AOP」玩具容器:只用 JDK 动态代理,为注册表里的每个接口提供一层「日志 + 计时」增强,并支持 -Dxxx.proxy=jdk|none 两种模式切换。
验收清单:
- [ ] 用
Map<String, Object>存 Bean,取用时若有切面就用Proxy.newProxyInstance现场包一层 - [ ] 连续两次
getBean返回同一个代理实例(缓存代理,而不是每次新建) - [ ] 代理能被强转成接口类型,但不能被强转成实现类——把这条差异写成一行的测试断言
- [ ] handler 里对
equals/hashCode/toString做特判,说明为什么要特判 - [ ] 说清一件事:如果某个实现类没有接口,这个玩具容器还能不能增强它?该怎么办?
能否口述 Proxy.newProxyInstance 三个参数的含义(类加载器、接口数组、handler),以及为什么第二个参数只能是接口?
$Proxy0 的方法体里那一行是什么?(h.invoke(this, method, args))——写出它,你就懂了 JDK 代理的全部。
Spring 选择 CGLIB 的三个条件分别是什么?(强制 proxyTargetClass/optimize/没有可用接口)
final 类与 final 方法的失败方式有何不同?(前者启动即抛、后者静默失效)
ClassCastException: cannot be cast to ...Impl 的两条自救路径是什么?(按接口注入;或打开 proxyTargetClass)
JDK 靠接口、CGLIB 靠继承;final 类当场炸、final 方法悄悄哑;替身转成本人必炸,自调用永远听不见。
动态代理是 Spring AOP 的引擎,它只有两条实现路线——JDK 代理靠 Proxy.newProxyInstance 生成实现接口的 $Proxy0,把调用转发给 InvocationHandler;CGLIB 靠 Enhancer 生成目标类的子类,用 MethodInterceptor 拦截。选型的决定性因素是「目标有没有接口」;Spring 默认有接口用 JDK、无接口用 CGLIB,Spring Boot 2.x 起默认 CGLIB。记住两条坑就够了:强转实现类只有 CGLIB 可行,final / private / static 方法无法被 CGLIB 拦截。