
前阵子面了一个三年经验的候选人聊到Spring的Transactional为什么能自动帮我们做事务提交和回滚他说是AOP。我再问AOP底层靠什么实现对方犹豫了一下说“应该是动态代理吧”但再往下问JDK动态代理和CGLIB有什么区别、代理对象是怎么生成的、为什么MyBatis只写一个Mapper接口就能执行SQL他就答不上来了。这个场景其实很典型。动态代理是Java面试中的高频考点也是Spring AOP、MyBatis Mapper、RPC框架、日志埋点、权限拦截这些基础设施的公共底座。你平时在项目里用的每一个Transactional、每一个Async、每一个FeignClient背后几乎都有动态代理在默默干活。但多数人对它的理解停留在“会用Proxy.newProxyInstance”这个层面对底层原理、框架选型逻辑、以及那些真正会踩到坑其实没怎么系统梳理过。这篇文章我就把动态代理从头到尾拆一遍包含JDK动态代理的手写实操、底层字节码生成逻辑、CGLIB的用法与对比、Spring和MyBatis里的真实应用场景以及我在实际项目中踩过的坑。内容不算短但对准备面试或者想真正理解框架底层的人来说值得耐心看完。1. 代理模式回顾为什么静态代理撑不住真实需求1.1 代理模式到底在解决什么问题先回到最朴素的问题。假设你现在写了一个UserService里面有一个saveUser方法上线之后发现每个方法都要加耗时统计和日志打印。最粗暴的做法是直接在每个方法里塞一段System.currentTimeMillis()和logger.info但这样业务代码会被大量非业务逻辑污染而且统计逻辑一旦变化所有方法都得跟着改。代理模式解决的就是这个问题。它不直接修改目标类而是创建一个代理对象让调用方和真实对象之间隔一层。代理对象负责在调用真实方法前后做增强处理比如打日志、做鉴权、控制事务。用代码说话代理模式的核心角色有三个抽象接口、真实对象、代理对象。抽象接口定义你能干什么真实对象是真正干活的代理对象是站在真实对象前面的“前台”帮你挡掉杂事。这种“不修改原代码却在原方法前后附加行为”的能力听起来很像AOP对吧其实AOP就是代理模式在横切逻辑上的经典应用。所以理解动态代理之前先把代理模式想明白后面就顺了。1.2 手写一个静态代理看看痛点在哪我写个小例子。假设有个发消息的功能public interface MessageSender { void send(String message); } public class SmsSender implements MessageSender { Override public void send(String message) { System.out.println(发送短信: message); } }静态代理类长这样public class SmsSenderProxy implements MessageSender { private final MessageSender target; public SmsSenderProxy(MessageSender target) { this.target target; } Override public void send(String message) { long start System.currentTimeMillis(); System.out.println(开始发送短信...); target.send(message); System.out.println(耗时: (System.currentTimeMillis() - start) ms); } }用起来也没问题但项目一旦复杂起来痛点就很明显如果MessageSender接口有二十个方法代理类里就得写二十个转发方法每个方法都要重复“记录时间-调用-计算耗时”这段逻辑。如果接口新增一个方法真实类和代理类必须同步修改否则编译都过不了。如果想让一个代理类同时代理多个不同类型的接口基本做不到因为代理类是在编译期写死的。静态代理把“增强逻辑”和“转发逻辑”耦合在了一起每增加一个接口就多一批模板代码。这时我们真正想要的是能不能在运行期动态生成一个代理类让它自动实现指定接口并且把每个方法调用都转发给同一个处理器这就是动态代理要干的事。2. JDK动态代理从0到手写核心API与第一个完整案例2.1 JDK动态代理的两个主角Proxy和InvocationHandlerJDK动态代理的核心只有两个东西但很多人过了很久都没完全理解它们的分工java.lang.reflect.Proxy负责在运行期生成代理类它是整个机制的入口。InvocationHandler你写的增强逻辑都放在它的invoke方法里它是代理行为的“大脑”。代理类生成之后它实现了你指定的接口但方法体里并不直接写业务逻辑而是把每一次方法调用都转交给InvocationHandler.invoke()。换句话说Proxy负责“造出代理对象”InvocationHandler负责“代理对象接到方法调用后该干什么”。这个设计非常巧妙它把“生成代理类”和“定义增强逻辑”拆成了两个独立维度。一个InvocationHandler可以代理任何接口只要Proxy能生成对应代理类一个Proxy生成的代理对象也可以搭配不同的InvocationHandler来改变行为。真正做到了一份增强逻辑到处复用。2.2 10分钟手写一个可运行的JDK动态代理我直接上一个完整案例场景就用当年最经典的“接口方法耗时监控”。先定义一个业务接口和实现类public interface UserService { void saveUser(String name); String getUserById(Long id); } public class UserServiceImpl implements UserService { Override public void saveUser(String name) { System.out.println(保存用户: name); } Override public String getUserById(Long id) { return user_ id; } }接下来写InvocationHandler这是整个代理逻辑的核心import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class TimeCostInvocationHandler implements InvocationHandler { private final Object target; public TimeCostInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); System.out.println(调用方法: method.getName()); // 通过反射调用真实对象的方法 Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(方法 method.getName() 耗时: cost ms); return result; } }然后写一个工具方法用于创建代理对象import java.lang.reflect.Proxy; public class ProxyFactory { public static Object createProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TimeCostInvocationHandler(target) ); } }测试一下public class Main { public static void main(String[] args) { UserService userService new UserServiceImpl(); UserService proxy (UserService) ProxyFactory.createProxy(userService); proxy.saveUser(张三); String user proxy.getUserById(1L); System.out.println(查询结果: user); } }输出结果调用方法: saveUser 保存用户: 张三 方法 saveUser 耗时: 8ms 调用方法: getUserById 查询结果: user_1 方法 getUserById 耗时: 2ms跑通这个例子之后你会发现动态代理的本质就一句话调用方持有的是代理对象代理对象把每次方法调用都转交给了InvocationHandlerHandler里做增强逻辑再通过反射把真正的方法调用落到目标对象上。2.3 invoke方法里的三个参数分别是什么invoke方法有三个参数每次写的时候都要理解它的含义proxy当前生成的代理对象本身。它和你在外部拿到的那个对象是同一个但基本不应该直接使用它。如果误用了很容易踩到无限递归的坑后面第7节详细说。method正在被调用的方法反射对象。通过它你可以拿到方法名、参数类型、注解也能通过method.invoke(target, args)调用真实对象的方法。args调用方法时传入的实际参数列表。没有参数时它是一个null或空数组所以写代码时最好做一下判空。我见过很多刚接触动态代理的人容易把method.invoke(target, args)写错成method.invoke(proxy, args)。这两种写法后果完全不同能把代码跑出StackOverflowError这个坑值得单独说。提示invoke方法内部无论多复杂最后一定要return真实方法的返回结果否则调用方拿到的永远是null程序行为会莫名其妙。2.4 为什么JDK动态代理要求目标类必须实现接口这是JDK动态代理最常被问到的一个限制。原因在于虚拟机对Proxy生成的代理类有硬性要求代理类本身已经继承了java.lang.reflect.Proxy类。Java是单继承的所以代理类没法再继承你的业务类只能通过实现接口来扩展能力。这就注定了JDK动态代理只能对“接口”做代理。如果你的目标是UserServiceImpl这样的具体类并且这个类没有实现任何接口那么target.getClass().getInterfaces()拿到的是空数组Proxy.newProxyInstance根本无从下手直接报IllegalArgumentException。那如果业务类就是没实现接口怎么办这就引出了后面要说的CGLIB动态代理。它不走接口路线而是通过生成目标类的子类来代理这正好绕开了JDK代理的单继承限制。3. 扒开JDK动态代理的底裤运行时字节码与类加载真相3.1 Proxy.newProxyInstance内部到底做了什么很多资料讲动态代理只讲到API使用层面对底层原理语焉不详。我把关键链路梳理一下看起来复杂其实核心只有四步第一步检查传入的接口列表。接口必须是接口类型、不能重复、不能是public接口但来自不同包否则直接抛异常。第二步查询代理类缓存。代理类生成一次之后会被缓存相同类加载器、相同接口列表的代理类下次直接从缓存取不用重复生成。第三步如果缓存没有调用ProxyClassFactory生成代理类的字节码。这一步是真正的技术核心它用ProxyGenerator.generateProxyClass()方法在运行时拼接出一个$Proxy0的字节码数组。第四步通过defineClass0这个native方法把字节码数组交给JVM定义成真正的Class对象然后反射实例化绑定InvocationHandler。这里注意一个细节动态代理虽然叫“动态”但并不是每次调用都去生成类。代理类只在第一次需要时生成后续通过缓存复用。这个缓存机制后面会专门展开。3.2 生成的代理类长什么样$Proxy0解剖JDK生成的代理类有一个通用命名模式com.sun.proxy.$Proxy0、$Proxy1。为了验证它的真实结构可以在运行时把生成的字节码保存下来然后反编译看内部实现。保存字节码有两种方式不同JDK版本参数不一样// JDK 9放在main方法里启动参数或直接System.setProperty System.setProperty(jdk.proxy.ProxyGenerator.saveGeneratedFiles, true); // JDK 8及更早 System.setProperty(sun.misc.ProxyGenerator.saveGeneratedFiles, true);保存之后反编译出来的$Proxy0结构大概是这样的简化版public final class $Proxy0 extends Proxy implements UserService { private static Method m1; // equals方法 private static Method m2; // toString方法 private static Method m3; // saveUser方法 private static Method m4; // getUserById方法 public $Proxy0(InvocationHandler h) { super(h); } public final void saveUser(String name) { try { super.h.invoke(this, m3, new Object[]{name}); } catch (RuntimeException | Error e) { throw e; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } } }从这个结构你可以看到几个关键信息代理类和目标类完全没有继承关系它只是和目标类实现了同一个接口。代理类构造器接收InvocationHandler并将它传给父类Proxy保存。每个接口方法内部都调用了super.h.invoke(this, method, args)把调用转交给InvocationHandler。原始方法Method对象被静态缓存了所以代理内部并没有走Class.getMethod()那种搜索过程效率比普通反射场景高一些。未受检异常直接抛出受检异常被包在UndeclaredThrowableException里。这就是为什么代理方法抛出受检异常时你用catch (Exception e)往往接不到原始异常类需要再unwrap一层。3.3 代理类缓存的实现细节JDK在Proxy类内部维护了一个WeakCacheClassLoader, Class?[], Class?类型的代理类缓存。这个设计有几个细节值得了解缓存key是类加载器和接口列表的组合。所以接口相同但类加载器不同会各自生成不同的代理类不会复用。缓存值存的是WeakReferenceGC时可能被回收。如果代理类被回收了下次获取会重新走生成逻辑。每个类加载器最多只能生成65535个代理类因为生成的类名$ProxyN里的N是一个short类型。这个缓存直接解释了一个经典问题为什么不是每次调用Proxy.newProxyInstance都会重新生成类。你真正常见到的代理对象其实只有那么几十个大量调用都在走缓存查询。这也是动态代理性能没有很多人想象中那么差的原因之一。3.4 从字节码层面看动态代理的性能问题很多人一听到动态代理就担心性能总觉得反射很慢。真实情况是JDK动态代理在8之后做了很多优化尤其是JVM对Method.invoke这一块的优化在JIT热点路径上已经非常快。而且代理类里Method对象是静态保存的省掉了方法查找开销。真正影响性能的不是反射本身而是你的InvocationHandler里做了什么。如果每次方法调用都在Handler里做大量字符串拼接、JSON序列化、DB查询那才是瓶颈。代理只是一个转发层不应该承载太多业务逻辑这一点在工程实践中非常重要。4. CGLIB动态代理实战与全面对比4.1 为什么非接口类也需要代理实际开发里大量对象是没有接口的。比如你引入了一个第三方jar包里面的核心类就一个普通类你想在调用它的方法时加监控JDK动态代理直接没法用。再比如Spring容器里很多Bean只声明了具体类而不是接口如果Spring AOP只能用JDK代理这些Bean就永远没法增强。CGLIB就是为了解决这个场景诞生的。它的思路是既然不能通过接口代理那就直接生成目标类的一个子类。子类继承目标类并重写目标类的非final方法。调用方拿着这个子类实例当作目标类使用方法调用被子类重写逻辑拦截然后在重写方法前后做增强。这里插一个知识点CGLIB底层是靠ASM字节码操作库来生成子类字节码的。它和JDK代理的区别本质上不是“有没有接口”而是一个走接口、一个走继承。4.2 用CGLIB手写一个动态代理案例先引入依赖。用Maven的话dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency注意Spring Boot的spring-core里已经内置了CGLIB的重打包版本所以很多场景你不需要额外引这个依赖。但手动学习时建议单独引不然容易和Spring内部版本搞混。写一个不需要接口的目标类public class OrderService { public void createOrder(String orderNo) { System.out.println(创建订单: orderNo); } public String getOrderStatus(Long orderId) { return PAID; } }CGLIB的核心API是Enhancer和MethodInterceptorimport net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class CglibProxyFactory { public static Object createProxy(Class? targetClass) { Enhancer enhancer new Enhancer(); // 设置父类 enhancer.setSuperclass(targetClass); // 设置回调 enhancer.setCallback(new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { long start System.currentTimeMillis(); System.out.println(CGLIB拦截方法: method.getName()); // 注意这里用的是 proxy.invokeSuper不是 method.invoke Object result proxy.invokeSuper(obj, args); System.out.println(方法耗时: (System.currentTimeMillis() - start) ms); return result; } }); return enhancer.create(); } }测试代码public class CglibMain { public static void main(String[] args) { OrderService proxy (OrderService) CglibProxyFactory.createProxy(OrderService.class); proxy.createOrder(NO123456); String status proxy.getOrderStatus(1001L); System.out.println(订单状态: status); } }输出结果CGLIB拦截方法: createOrder 创建订单: NO123456 方法耗时: 12ms CGLIB拦截方法: getOrderStatus 订单状态: PAID 方法耗时: 3ms这里要重点提示一个和JDK代理不同的地方CGLIB的MethodInterceptor.intercept方法里有四个参数其中第四个参数MethodProxy是CGLIB为每个方法额外生成的索引调用代理。它调用真实方法的方式是proxy.invokeSuper(obj, args)而不是JDK代理里那种method.invoke(target, args)。这个区别非常关键因为invokeSuper走的是CGLIB优化过的FastClass机制性能上通常比纯JDK反射要好。顺带说一句method.invoke(obj, args)在CGLIB里也可以调用真实方法但因为obj本身是子类实例实际调用会再次进入intercept逻辑导致无限递归。所以不要混用否则你会收获一个StackOverflowError。4.3 JDK动态代理和CGLIB对比一张表说清楚很多人在面试时被问“JDK代理和CGLIB选哪个”其实只要把下面这张表想明白就基本能回答到位。对比维度JDK动态代理CGLIB动态代理代理原理运行期生成实现接口的代理类运行期生成目标类的子类目标要求目标必须实现接口目标类不能被final修饰方法要求代理所有接口方法final方法无法被代理生成方式字节码拼接JVM内部完成基于ASM字节码操作生成子类调用方式反射Method.invokeFastClass索引直接方法调用性能表现初始化开销较小长期调用经过JIT优化后不差生成类开销略大运行期FastClass调用通常更快依赖关系JDK自带无额外依赖需要引入cglib依赖使用场景有接口的Spring Bean、MyBatis Mapper等无接口的具体类、第三方库类有一个容易记反的点CGLIB运行期性能通常比JDK代理更好但它的初始化开销更高因为生成子类要做更多字节码操作。Spring的官方文档里对此有过讨论但到了现代JDK版本两者的差距已经没那么大。工程上优先考虑的是“目标类有没有接口”而不是单纯为了性能选型。4.4 Spring Boot为什么默认使用CGLIBSpring的AOP历史上支持两种代理模式。默认行为在不同版本有变化Spring Boot 1.x如果目标类实现了接口默认走JDK动态代理没有接口才走CGLIB。Spring Boot 2.x及之后默认开启spring.aop.proxy-target-classtrue也就是只要Spring容器里有AOP需求统一使用CGLIB。这个变化背后有一个很现实的原因JDK代理要求接口如果某个Bean没有接口之前Spring会悄悄降级到CGLIB但等大家发现时往往已经产生了“某些Bean被代理、某些Bean没被代理”的不一致行为。统一强制CGLIB之后行为一致性大大提升代价是引入了CGLIB的那些限制无法代理final类、final方法但绝大多数业务Bean根本不会被final修饰所以这个代价在工程上完全可接受。5. 动态代理在主流框架里的真实打开方式5.1 Spring AOP一次调用穿越多个代理的拦截链路Spring AOP在运行时做两件事判断Bean是否满足切点表达式如果满足就为它生成代理对象然后把代理对象放进容器。后面所有依赖注入拿到的都是这个代理对象而不是原始Bean。这里有一个细节值得展开。当多个切面同时作用于一个Bean时Spring会叠加代理。比如先加事务切面再加日志切面调用顺序可能是这样调用方 - 日志代理 - 事务代理 - 真实Bean方法这个链条能成立靠的就是动态代理的多层包装。每个切面各自生成一个代理对象前一个代理的InvocationHandler里持有下一个代理对象的引用一层层forward下去。但多代理也有一个副作用如果某层代理把异常给吞了后置切面逻辑不会执行如果切面里抛出的异常类型和处理顺序设计得不好事务可能一直回滚到你怀疑人生。Spring AOP虽然封装了复杂度但理解代理链背后的转发逻辑排查问题时非常有帮助。5.2 MyBatis Mapper只有接口没有实现类为什么能执行SQLMyBatis是动态代理最惊艳的应用之一。你在项目里写了一个UserMapper接口里面声明了User selectById(Long id)方法但你没有写任何实现类为什么Spring注入Mapper的时候能拿到一个可用对象原因就是MyBatis在启动时扫描Mapper接口针对每个接口调用Proxy.newProxyInstance生成代理对象然后把Mapper方法的配置对应的SQL语句、参数映射、返回类型解析好之后绑定到一个MapperProxy的InvocationHandler里。当你调用selectById时调用会被转发到MapperProxy.invoke它根据方法名去查找对应的SQL通过SqlSession执行查询最后把ResultSet映射成User对象。如果试着简化一下其实核心逻辑长得像这样public class MapperProxyT implements InvocationHandler { private final ClassT mapperInterface; private final SqlSession sqlSession; Override public Object invoke(Object proxy, Method method, Object[] args) { // 根据方法签名找到对应的MappedStatement MappedStatement ms sqlSession.getConfiguration() .getMappedStatement(mapperInterface.getName() . method.getName()); // 执行SQL并返回映射结果 return sqlSession.selectOne(ms, args null ? null : args[0]); } }这个场景里动态代理的价值体现得淋漓尽致框架只需要知道接口定义就能在运行时替我们生成“实现类”。业务侧连一个实现都不用写整个数据访问层就自动跑起来了。5.3 其他常被忽略的动态代理应用场景除了Spring和MyBatis还有几个常见场景值得知道Feign/RPC框架声明一个远程接口不需要实现框架动态生成代理把方法调用转换为网络请求。日志切面统一打印接口入参出参不用每个方法手动打日志。权限校验方法级别的权限拦截在代理层校验当前用户是否有权限执行。缓存切面方法级缓存命中缓存直接返回不命中再调真实方法并回填缓存。幂等控制、分布式锁、接口限流这类横切逻辑都适合在代理层统一处理业务代码保持干净。你会发现所有这些场景有个共同点它们都是横切逻辑不适合写在业务代码里而动态代理提供了一种“不动业务代码、把公共逻辑插进去”的机制这正是AOP思想的技术基础。6. 动态代理的七个经典坑几乎人人都踩过6.1 在invoke里错误引用proxy导致无限递归这是动态代理新手最容易踩的坑。代码可能长这样public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 错误写法proxy.getClass()或method.invoke(proxy, args) Object result method.invoke(proxy, args); return result; }问题在于proxy本身就是代理对象对它再调用同一个方法时又会进入invoke然后再调用proxy无限循环下去直到栈溢出。正确的做法一定是把真实对象目标对象存下来通过method.invoke(target, args)调用。唯一的例外是你故意要在代理对象之间做转发但那是设计过的链路不是随手写出来的。6.2 Spring事务中this调用导致增强失效这个坑在Spring项目里非常高频。你写了一个ServiceService public class UserService { public void process() { this.save(); // 事务会失效 } Transactional public void save() { // 数据库操作 } }调用process()时外部进来的是代理对象但this.save()里的this是原始Bean对象不是代理对象所以save方法上的Transactional根本没有机会生效事务不会开启。解决办法有几个。最简单的是把save方法拆到另一个Bean里通过注入调用也可以在Spring配置spring.aop.proxy-target-classtrue的前提下使用AopContext.currentProxy()((UserService) AopContext.currentProxy()).save();前提是启动类上开启EnableAspectJAutoProxy(exposeProxy true)。理解这个问题的关键还是想清楚“外部持有的究竟是不是代理对象”这个点。6.3 JDK代理对象只能强转为接口类型有人这样写直接崩溃UserServiceImpl impl (UserServiceImpl) proxy; // ClassCastExceptionJDK生成的代理对象是$Proxy0它和UserServiceImpl没有任何继承关系只实现了UserService接口。它只能强转为接口类型不能强转为实现类类型。如果代码里大量直接引用了实现类建议先重构为面向接口编程否则动态代理根本玩不转。6.4 CGLIB无法代理final方法和final类CGLIB靠生成子类实现代理final类不能被继承final方法不能被重写这是硬限制。如果你的目标类或者方法被final修饰CGLIB会直接报错或者静默跳过增强。我曾经在一个老项目里排查了很久的诡异Bug最后发现是一个基础Service类里几个关键方法被final修饰了CGLIB代理生成了子类但无法重写这些方法导致切面逻辑时而生效时而不生效。潜在教训如果你确定这个类需要被AOP增强就别给方法加final或者至少评估一下代理方式的兼容性。6.5 JDK代理对Object类方法的处理容易被忽略equals、hashCode、toString这三个Object方法在代理调用链中同样会被转发到invoke。如果你的InvocationHandler里没处理这几个方法代理对象的行为会变得很怪。举个实际例子两个代理对象包装同一个目标对象如果equals方法没有比对内部target比较结果可能是false如果不小心在invoke里调用了proxy.toString()还会导致递归栈溢出。一个稳妥的处理方式是在invoke里对方法名做一个判断对Object的这几个方法直接调用method.invoke(target, args)或者直接返回由InvocationHandler自定义的实现不要无脑转发。6.6 代理对象序列化与类加载器隔离的坑分布式项目里如果要对代理对象做序列化会发现JDK生成的代理类虽然实现了Serializable经由Proxy但InvocationHandler多持有目标对象引用序列化时会连带序列化一大坨内部状态甚至可能因为目标对象不可序列化而直接失败。更隐蔽的问题是类加载器隔离代理类通过指定ClassLoader加载和缓存如果同一个接口由两个不同的ClassLoader加载会生成两个不同的代理类。在OSGi、应用热部署这类场景容易出现类型转换异常。6.7 依赖注入的“代理陷阱”Spring容器里如果某个Bean被代理了通过Autowired注入时注入的是代理对象而不是原始对象。如果你在代码里做过类型判断比如bean instanceof UserServiceImpl这个判断在CGLIB代理下是true因为子类是目标类的子类但在JDK代理下是false因为代理类只实现了接口不继承实现类。这会导致同一种逻辑在不同代理模式下行为不一致。另外还要注意如果项目里自己手动用Proxy.newProxyInstance生成代理对象再交给Spring管理原始目标类和代理对象可能同时存在于容器中。如果你没有区分名字注入时可能拿到的是没有经过任何增强的原始对象这也是很多人排半天查不出来的隐藏问题。7. 工程视角下的选型建议与学习路径7.1 什么时候用JDK动态代理什么时候用CGLIB一句话结论能面向接口就面向接口用JDK动态代理。原生支持、无额外依赖、调试方便、和Spring的AOP接口对接顺畅。目标类没有接口、第三方库类无法改代码时用CGLIB。比如你在扩展某个开源工具类的行为时。在一个团队项目里尽量保持代理模式统一。不要一会儿JDK一会儿CGLIB否则各种类型判断、序列化、调试体验都会混乱。如果是在Spring Boot 2.x环境里做AOP直接用Spring默认的CGLIB不用纠结。7.2 第三股势力AspectJ和Byte Buddy动态代理之外还有更底层的字节码增强技术比如AspectJ在编译期直接修改类字节码Byte Buddy在运行期操作字节码。它们可以做动态代理做不到的事比如修改静态方法、修改类的字段定义、增强构造方法甚至拦截一个类的new操作。从工程角度看动态代理适合方法级别的横切逻辑字节码增强适合更底层的框架能力。Spring AOP本身基于动态代理但它也支持通过EnableLoadTimeWeaving接入AspectJ的编译期机制这两种方式各有取舍。一般业务开发用Spring AOP就够了真要玩字节码增强建议先把ASM或者Byte Buddy的基础手写几遍再深入AspectJ。7.3 想彻底掌握动态代理我建议按这个顺序练习第一步把JDK动态代理的完整案例手写三遍不看书的情况下能独立写完。第二步写一个带事务模拟的代理层。不真的连DB用ThreadLocal模拟事务状态体会代理层如何统一控制提交回滚。第三步手写一个极简AOP框架。定义Log注解和Transactional注解用动态代理扫描被注解注释的Bean并生成代理对象这个练习做完Spring AOP的很多疑问会迎刃而解。第四步用CGLIB代理改造同一个练习体验无接口场景下的代理差异。第五步打开JDK的Proxy源码和CGLIB的Enhancer源码对着刚才写的代码把类生成、缓存、调用链逐行过一遍。我一直觉得框架源码看得再多不亲手写一轮代理很多东西都是浮在表面的。尤其“代理对象和真实对象到底是什么关系”这个感觉光靠读源码建立不起来必须自己把代理对象接过来调一调在断点里看一遍调用链理解才算真正落地。8. 写在最后从面试答案到实战能力的跃迁动态代理这个知识点面试可以只考到“JDK和CGLIB的区别”但真正能拉开差距的是你能不能从代理对象、InvocationHandler、类生成、缓存、AOP链路这条线把一个方法调用从进入到返回的完整路径讲清楚。我在实际项目里帮同事排查过不少诡异问题最后都绕回到“在座各位拿到的到底是不是代理对象”这件事上。比如某个Service的方法事务不生效第一反应就该查它是不是被this调用了某个接口注入后强转报错先想想它是不是被JDK代理了某个CGLIB代理对象方法没有拦截去看看目标方法是不是final了。如果你正在准备面试我的建议是把整篇文章里的代码自己敲一遍然后尝试不看资料解释清楚这三个问题JDK动态代理的代理类是如何生成的、为什么必须有接口、Spring里JDK代理和CGLIB代理是如何相互配合的。能把这几个问题讲明白动态代理这个知识点基本就过关了。而对已经在写业务代码的人来说找到自己项目里现有的代理使用场景打开Spring的AOP配置看一看当前走的是哪种代理模式再结合今天拆的底层逻辑应该会有一种“原来之前那些黑盒都是这么回事”的感觉。技术这东西看一遍不如写一遍自己动手生成一个代理出来比背十遍八股文都要扎实。