JDK动态代理与CGLIB对比:原理、源码、性能与实战排查
做Java开发这些年动态代理是我见过出场率最高、却最容易被会用两个字糊弄过去的技术之一。面试聊Spring AOP会问到它排查事务不生效要检查它读MyBatis源码会发现它甚至写个简单的日志切面本质上也是在跟它打交道。很多人Aspect用得飞起但一旦被问到JDK动态代理和CGLIB到底有什么区别瞬间就卡壳了。这篇文章我不打算只给结论。我会把JDK原生动态代理和CGLIB动态代理从使用方式、源码角度、字节码层面、性能差异、实际场景到踩坑实录完整拆一遍。目标是让你读完不仅能写出来还能在面试里把原理讲明白在真出问题时知道往哪个方向排查。零基础没关系我会把每一步都铺开讲有经验的也可以直接跳到第4节和第6节那里有我实际项目中总结出来的对比和排查干货。1. 动态代理到底在解决什么问题1.1 没有动态代理时业务代码是怎么被污染的先回到最原始的场景。假设你要维护一个用户服务本身只需要查数据、存数据但公司要求所有方法必须打印日志、统计耗时。没接触过代理之前很多人会直接在业务方法里加代码public User findById(Long id) { long start System.currentTimeMillis(); System.out.println([日志] 开始查询用户参数 id); try { User user userMapper.selectById(id); System.out.println([日志] 查询成功结果 user); return user; } finally { System.out.println([日志] 方法耗时 (System.currentTimeMillis() - start) ms); } }单个方法这么写还能忍但当你负责的Service里有几十个方法时这段日志代码会复制得铺天盖地。更要命的是第二天产品说日志格式要加一个traceId你得把所有方法改一遍——这就是典型的横切逻辑和业务逻辑耦合。这里要引入一个概念日志、权限校验、事务管理这类逻辑不归属于任何一个业务模块但它们要织入到许多业务模块中被称为横切关注点。动态代理的核心价值就是把这些横切逻辑从业务代码中剥离出来统一放到代理对象里处理。1.2 静态代理先顶一顶但上限明显在动态代理出现之前最朴素的做法是静态代理给每个接口手动写一个代理类代理类里持有真实对象在调用前后插入逻辑。public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public User findById(Long id) { long start System.currentTimeMillis(); try { return target.findById(id); } finally { System.out.println(耗时 (System.currentTimeMillis() - start) ms); } } // 其他接口方法都要重复实现一遍... }静态代理的问题一眼就能看出来每个接口对应一个代理类代理方法里大量重复逻辑接口一旦新增方法代理类必须同步修改。而且它是在编译期确定好的运行时想替换策略没门。用一句话概括静态代理把横切逻辑从业务代码中搬出来了但又把麻烦转移给了代码维护者。1.3 动态代理的价值运行时生成调用方无感知动态代理的思路完全不一样——它不在编译期写死代理类而是在程序运行过程中由JVM动态生成一个代理对象。这个代理对象和目标类实现了相同的接口或者继承了目标类调用方拿到它之后根本感觉不到这是一个假的对象。你调用代理对象的任何方法都会先经过一段统一拦截逻辑通常叫InvocationHandler或MethodInterceptor由这段拦截逻辑决定是要前置增强、后置增强、还是直接短路不调用真实方法。Spring AOP、MyBatis的Mapper代理、RPC中的透明远程调用全都是这个思路的具体实践。Java世界里有两套主流实现JDK官方提供的基于接口的动态代理以及基于字节码生成子类的CGLIB。下面两节我会分别往深里拆。2. JDK原生动态代理从入门到源码级理解2.1 核心角色只有三个InvocationHandler、Proxy、目标对象的接口JDK动态代理涉及的类非常少核心就三个要素。第一个是InvocationHandler接口它定义了一个invoke方法相当于代理对象的总拦截器。你传给它的任何方法调用都要先经过这里处理public interface InvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }第二个是Proxy类它是JDK创建代理对象的入口。最关键的方法是newProxyInstance三个参数分别是类加载器、要代理的接口数组、以及调用处理器。第三个要素是目标对象实现的接口。这里要划重点JDK动态代理本质上是代理接口不是代理类。目标对象必须实现至少一个接口代理对象才能以这些接口的类型暴露出去。这三个角色凑在一起就构成了一条完整的调用链调用方持有一个接口类型的引用实际指向代理对象代理对象的内部持有InvocationHandlerInvocationHandler内部持有真实的目标对象。任何方法调用都会从接口穿透到代理对象再转发给InvocationHandler由它决定是否调用真实目标。2.2 完整实现一个带日志增强的用户服务光讲概念不够我把完整代码贴出来你直接复制就能跑。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; public class JdkDynamicProxyDemo { // 1. 业务接口JDK动态代理的前提条件 public interface UserService { User findById(Long id); void createUser(User user); } // 2. 目标实现类真正干活的对象 public static class UserServiceImpl implements UserService { Override public User findById(Long id) { System.out.println(进入真实方法查询用户 id); return new User(id, 张三); } Override public void createUser(User user) { System.out.println(进入真实方法创建用户 user.getName()); } } // 3. 简单数据模型 public static class User { private Long id; private String name; public User(Long id, String name) { this.id id; this.name name; } public Long getId() { return id; } public String getName() { return name; } } // 4. 调用处理器所有增强逻辑的集中地 public static 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 { long start System.currentTimeMillis(); System.out.println([前置增强] 调用方法: method.getName() , 参数: Arrays.toString(args)); // 核心一步通过反射调用真实目标的方法 Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println([后置增强] 方法结束耗时: cost ms); return result; } } public static void main(String[] args) { // 设置文件保存路径后可以把JVM生成的代理类写到磁盘 System.setProperty(jdk.proxy.ProxyGenerator.saveGeneratedFiles, true); UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.findById(1L); proxy.createUser(new User(2L, 李四)); } }运行输出大概是这样的[前置增强] 调用方法: findById, 参数: [1] 进入真实方法查询用户 1 [后置增强] 方法结束耗时: 0ms [前置增强] 调用方法: createUser, 参数: [User{id2, name李四}] 进入真实方法创建用户 李四 [后置增强] 方法结束耗时: 0ms注意几个细节。invoke方法的第一个参数proxy是代理对象本身在大多数场景用不到但要注意在invoke内部不要再调用proxy的任何方法否则会陷入无限递归。第二个参数method是当前被调用的接口方法可以通过method.getName()等方法名做精细化拦截。第三个参数args是调用参数反射转发给真实对象时原样传入。2.3 生成的代理类长什么样反编译看真相JDK动态代理最让人迷惑的地方是那个看不见的代理类到底长什么样。其实JVM在运行时会为每一组接口生成一个名为$Proxy0、$Proxy1这样的类文件。只要在启动时加上系统属性就能把它保存下来。不同JDK版本的设置方式略有区别JDK 8及以下用sun.misc.ProxyGenerator.saveGeneratedFilesJDK 9以上用jdk.proxy.ProxyGenerator.saveGeneratedFiles也可以像我的示例里那样直接System.setProperty。保存下来的类反编译后大体是这样一个结构public class $Proxy0 extends Proxy implements UserService { private static Method m1; private static Method m2; // ... public $Proxy0(InvocationHandler h) { super(h); } public final User findById(Long id) { try { return (User) super.h.invoke(this, m1, new Object[]{id}); } catch (RuntimeException | Error e) { throw e; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } } }看懂这个结构很多问题就豁然开朗了。第一生成的$Proxy0继承了java.lang.reflect.Proxy而Java是单继承的所以它不可能再继承任何业务类唯一能做文章的就是实现接口。这就解释了JDK动态代理为什么必须要有接口。第二类的构造方法只有一个接收一个InvocationHandler参数。findById方法内部做的事非常朴素把接口方法对应的Method对象和参数打包交给super.h.invoke去处理。这里的h就是你在newProxyInstance时传入的调用处理器。第三所有方法都声明为final也就是说这个代理类本身也不能再被继承或覆盖。代理对象的所有方法签名在生成那一刻就固定了增强逻辑只存在于InvocationHandler里。还有一个有意思的细节JDK生成的代理类会对方法做分组缓存同一个接口的不同方法会对应不同的静态Method对象这些在类加载时就已经准备好了。所以你调用代理方法时并不需要每次重新通过方法名去反射查找这一层开销被优化掉了。真正产生的反射调用发生在method.invoke(target, args)这一行。2.4 为什么JDK动态代理只能是接口代理这个问题我被问过无数遍。其实一句话就能说透JDK动态代理生成的代理类必须继承Proxy这个类Java不允许多重继承于是它失去了继承业务类的资格。要想让调用方把代理对象当成业务对象来用只能让代理类去实现业务对象所属的接口这样调用方的引用类型就是接口接口和代理类之间天然兼容。所以如果你的目标类压根没实现任何接口JDK这条路直接死掉只能走CGLIB。如果你的目标类实现了接口使用JDK动态代理时调用方持有的引用也必须是接口类型而不能是具体实现类类型否则会出现第6节要说的ClassCastException。从设计者的角度看JDK动态代理其实非常克制它只做接口层面的代理不碰具体类。这种约束看似是限制却也带来了一个好处——代理类和目标类完全解耦只依赖接口契约。这也是Spring默认在目标实现接口时优先选择JDK动态代理的原因之一。3. CGLIB动态代理子类增强的威力与局限3.1 CGLIB的底层原理运行时生成一个子类CGLIB的全称是Code Generation Library它不依赖JDK的Proxy机制而是用字节码技术直接在运行时生成目标类的子类。这个子类继承了目标类的方法并重写那些需要增强的方法。当调用方持有这个子类对象时调用的实际上是重写后的方法重写逻辑里会先走MethodInterceptor再通过MethodProxy调用父类的原始方法。要理解CGLIB为什么能干这件事得先知道Java类加载的一个基本能力JVM允许在运行时通过ClassLoader加载一段动态生成的字节码。CGLIB底层用的是ASM一个轻量级字节码操作框架它不经过Java编译器直接操作字节码指令拼一个子类的class文件出来再加载进JVM。整个过程对使用者完全透明你看到的只是Enhancer和MethodInterceptor两个核心接口。这个方案从原理上就绕开了必须实现接口的约束。任何非final的普通类理论上都可以被CGLIB增强。代价是如果目标类本身是final的或者某个方法是final的子类无法继承和重写那就无能为力。3.2 完整实现Enhancer MethodInterceptor MethodProxyCGLIB的API使用起来同样简洁。需要说明的是如果你在Spring环境下开发spring-core里已经内嵌了一份重命名版的CGLIB包名变成了org.springframework.cglib直接依赖Spring就能用。独立使用时要单独引入依赖。下面是完整的独立示例。import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; import java.util.Arrays; public class CglibProxyDemo { // 目标类注意不需要实现任何接口 public static class UserService { public User findById(Long id) { System.out.println(进入真实方法查询用户 id); return new User(id, 王五); } public void createUser(User user) { System.out.println(进入真实方法创建用户 user.getName()); } // final方法演示无法被增强的场景 public final void logFinal() { System.out.println(这是一个final方法调用时不会经过拦截器); } } public static class User { private Long id; private String name; public User(Long id, String name) { this.id id; this.name name; } public Long getId() { return id; } public String getName() { return name; } } // 方法拦截器 public static class LogMethodInterceptor implements 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() , 参数: Arrays.toString(args)); // 注意这里是调用父类方法不是method.invoke Object result proxy.invokeSuper(obj, args); long cost System.currentTimeMillis() - start; System.out.println([后置增强] 方法结束耗时: cost ms); return result; } } public static void main(String[] args) { // 保存生成的字节码文件便于调试 System.setProperty(cglib.debugLocation, ./cglib_classes); Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new LogMethodInterceptor()); UserService proxy (UserService) enhancer.create(); proxy.findById(1L); proxy.createUser(new User(2L, 赵六)); proxy.logFinal(); // 这行不会走拦截器 } }运行结果是[前置增强] CGLIB调用: findById, 参数: [1] 进入真实方法查询用户 1 [后置增强] 方法结束耗时: 0ms [前置增强] CGLIB调用: createUser, 参数: [User{id2, name赵六}] 进入真实方法创建用户 赵六 [后置增强] 方法结束耗时: 0ms 这是一个final方法调用时不会经过拦截器对比JDK动态代理CGLIB有两个明显的不同点。第一intercept方法的参数里多了一个MethodProxy它是CGLIB性能优化的关键下面单独讲。第二调真实方法用proxy.invokeSuper(obj, args)它直接调用父类方法不会再次触发拦截器。如果你换成method.invoke(obj, args)由于obj是代理子类对象方法的动态分派会再次命中拦截器直接死循环。这是我见过最高频的新手错误务必记住。3.3 FastClass机制为什么CGLIB调用比反射快CGLIB经常被拿来和JDK动态代理比性能核心争议点就在MethodProxy和FastClass这里。传统反射调用的大致路径是通过Method对象找到方法句柄做权限检查、参数解析然后才真正调用。CGLIB的做法是在生成子类的同时额外生成两个FastClass类一个是目标类的一个是代理类的。这两个类内部维护了一个方法索引表把每个方法映射成一个数值索引。调用时直接按索引进入对应的invoke方法中间省去反射的那一系列检查流程。用代码举例proxy.invokeSuper(obj, args)底层大致走的是根据方法签名找到索引然后通过索引直接进入父类调用逻辑。整个调用链路更短性能通常优于JDK动态代理的反射调用。尤其在高频调用场景下这种优势会被放大。不过这不等于CGLIB全面碾压JDK动态代理创建代理对象时CGLIB需要额外生成子类和FastClass类文件首次创建的开销明显比JDK动态代理大。所以很多框架的实现都是优先JDK动态代理不能代理时再退CGLIBSpring就是这么干的。3.4 CGLIB的局限和隐藏坑CGLIB靠继承实现增强这是它的最大优势也埋下了不少坑。首先是最直观的final限制。final类不能被继承final方法不能被重写这两个场景CGLIB直接失效。如果你的目标类里有final方法并且你希望它也被增强只能重构设计去掉final。其次是包级私有方法的问题。CGLIB生成的代理子类默认放在目标类的同一个包下这样才能访问包级方法。如果你希望增强一个包级方法代理子类却和它不在同一个包那就做不到。实际业务中这种场景不常见但了解根因后碰到诡异问题时思路会清晰很多。第三个坑和JDK模块强封装有关。JDK 16开始默认对java.base模块实施强封装CGLIB底层使用反射访问某些JDK内部信息时会抛出InaccessibleObjectException。JDK 17及以后更严格如果直接使用CGLIB很可能需要添加--add-opens参数。Spring Boot在2.x后期已经做了适配内部处理了大量这类兼容性问题。这个坑后面第6节细说。4. 两大动态代理横向对比与选型建议4.1 机制、限制、性能的全维度对比把两者的核心差异整理成一张表面试时照着这张表说基本能过关对比维度JDK动态代理CGLIB动态代理代理对象形态实现目标接口继承目标类是否要求接口必须实现接口无要求普通类即可底层技术字节码生成 反射ASM字节码生成 FastClass索引增强方式通过InvocationHandler通过MethodInterceptor调用目标方法method.invoke(反射)proxy.invokeSuper(索引调用)final类/方法不影响代理的是接口无法代理代理对象类型只能转成接口类型可转成目标类类型创建代理的耗时较低较高首次生成字节码方法调用的性能略逊于CGLIB反射开销更优FastClass索引三方依赖JDK自带无第三方依赖需引入或使用Spring内嵌版本理解这张表有个关键点要记住JDK动态代理和CGLIB不是竞争关系而是互补关系。目标实现了接口你用JDK动态代理目标没有接口或需要代理非接口类你用CGLIB。Spring的AOP就是按这个逻辑动态选择的。4.2 实际项目中如何选型我见过很多团队一上来就把spring.aop.proxy-target-classtrue设成全局强制所有代理都用CGLIB。这么做的理由是CGLIB性能好但真去分析大部分项目里AOP方法调用的频率并没有高到能感知反射和FastClass的差异而强制CGLIB反而会带来额外的代理类生成开销、final方法不可代理的隐患。个人建议按这个顺序决策目标有接口首选JDK动态代理目标没有接口或者需要代理的是第三方类库里的非接口类用CGLIB如果框架已经帮你封装好了比如Spring AOP保持默认策略不要轻易全局改开关。把注意力放在业务逻辑本身代理机制的差异在绝大多数场景下都不该成为瓶颈。4.3 Spring的自动选择策略Spring AOP不是一个独立的代理实现它内部集成了两套机制当目标对象实现了接口时默认使用JDK动态代理没有实现接口时退化为CGLIB。具体决策的地方在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); } return new JdkDynamicAopProxy(config); }这段逻辑透露了几个信息。第一Spring会判断目标类是不是接口、是不是已经是被代理过的类第二如果配置了proxy-target-classtrue优先CGLIB第三目标类没有实现任何接口时哪怕是普通类也走CGLIB。在Spring Boot 2.x中如果你不显式配置并且目标实现了接口JDK动态代理会成为最终选择。这也是为什么很多新手在给Service写AOP时发现注入的类型不能是具体Service类而必须是接口的原因。5. 动态代理的经典实战场景5.1 Spring AOP切面编程的地基Spring AOP是动态代理最典型的应用。声明式事务管理、方法级日志、接口耗时监控、权限校验这些横切逻辑全部通过AOP切面完成。你定义一个Around通知Spring在启动时扫描到切面表达式匹配的Bean会为其生成代理对象这个代理对象被注入到其他依赖方。在这个机制里一个容易忽略的细节是代理与自调用的问题。假设一个Service内部的methodA调用了同一个类的methodBmethodB上标了Transactional。由于methodB是通过this直接调用的而不是通过代理对象调用的拦截器根本感知不到这次调用事务自然不生效。解决方案通常是注入代理对象自己或者把事务边界放在外部调用的方法上。这类问题排查起来极为痛苦根因就是动态代理只拦截外部入口。5.2 MyBatis Mapper没有实现类的接口是怎么干活的用过MyBatis的人都知道Mapper接口不需要写实现类直接注入就能用。这背后的核心就是动态代理。MyBatis会为每个Mapper接口创建一个MapperProxy这是个InvocationHandler实现。当调用mapper.selectUserById(id)时实际进入的是MapperProxy.invoke方法它会根据接口的全限定名加方法名拼接出SQL语句ID然后交给SqlSession执行。public class MapperProxyT implements InvocationHandler { private final ClassT mapperInterface; private final SqlSession sqlSession; Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } String statementId mapperInterface.getName() . method.getName(); // 根据返回类型决定调用selectOne还是selectList if (List.class.equals(method.getReturnType())) { return sqlSession.selectList(statementId, args null ? null : args[0]); } return sqlSession.selectOne(statementId, args null ? null : args[0]); } }这个模式让人印象深刻的地方在于它让接口即约定这件事变得无比纯粹。开发人员只关心接口定义MyBatis在运行时替它生成实现。这不只是省掉了一个实现类的问题而是让整个持久层设计和数据库操作解耦了。5.3 RPC框架与Spring Cloud中的透明远程调用在RPC场景里调用一个远程服务往往和调用本地方法一样这个透明感也来自动态代理。比如声明一个远程接口框架在启动时通过Proxy.newProxyInstance生成代理对象注入到业务代码中业务代码调用接口方法时代理对象的invoke会把方法名、参数类型、参数值序列化发起网络请求到远端再反序列化返回结果。整个过程业务代码无感知异常处理、超时重试、负载均衡都封装在代理里。这种设计的核心价值是调用方不需要关心网络细节只依赖一个接口契约。接口是唯一的强约束具体的通信协议、注册中心、序列化方案都在代理层透明完成。这也是很多微服务框架的共同思路——用动态代理抹掉本地调用和远程调用的边界。5.4 通用增强日志、权限、缓存抛开框架动态代理本身就是一个天然的增强器。权限校验可以做成代理只有登录用户才能调用某些方法否则直接抛出异常无需在业务代码里写if判断。缓存也可以做成代理第一次调用真实方法并缓存结果之后命中缓存直接返回业务方法自己完全不感知。日志和耗时统计是日常最常用的第一节的例子已经演示了。把这些场景统一起来看动态代理做的事情本质上是在不修改源代码的情况下给对象的方法调用动态插入额外行为。它让程序员可以以组合的方式扩展对象能力而不是用继承静态绑定。配合注解使用还可以把增强逻辑做得非常优雅——方法上打一个Metric代理类读取注解并决定是否统计耗时业务代码里干干净净。6. 常见问题与排查技巧实录6.1 ClassCastException$Proxy0 cannot be cast to UserServiceImpl这是JDK动态代理最经典的报错。原因很简单JDK生成的代理类继承的是Proxy不是你的实现类它唯一能向上转型的形态就是你传进去的接口。如果你写的是UserServiceImpl impl (UserServiceImpl) proxy;必然失败。解决办法接收代理对象时永远用接口类型。这就是为什么依赖注入时Spring文档建议面向接口编程IoC容器注入的代理对象才能顺利适配。排查这类问题时先看变量声明类型是不是接口再看目标类是否真的实现了接口。6.2 InvocationTargetException反射调用的双重包装在用JDK动态代理调用真实方法时如果目标方法抛出业务异常反射机制会把它包装成InvocationTargetException抛出。很多人在AOP通知里捕获异常时发现类型不对或者拿不到原始异常消息根因就是这层包装。解决办法在代理逻辑中解包一层再抛出或处理try { return method.invoke(target, args); } catch (InvocationTargetException e) { throw e.getTargetException(); }拿真实异常给上层处理业务才能正确感知。如果你在日志里看到Caused by: InvocationTargetException先怀疑这个位置。6.3 CGLIB代理final方法失效不是Bug是机制限制CGLIB靠生成子类重写方法实现增强final方法是Java的硬性约束不可能被子类重写。所以如果你的配置类或方法标了final然后期待切面拦截它不会等来报错只会静默失效。这类问题最让人头疼的地方在于不报错但没效果。排查思路确认目标类和目标方法不是final确认目标类不是final class。另外还要注意CGLIB重写方法时使用的可见性规则私有方法也无法增强因为它们根本不会被子类重写。6.4 Java 17强封装CGLIB抛出InaccessibleObjectException从JDK 16开始默认强封装java.base模块。CGLIB底层用反射处理一些JDK内部字段和方法时可能触发InaccessibleObjectException。独立使用CGLIB时需要给JVM加启动参数--add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.lang.reflectALL-UNNAMED使用Spring Boot的项目一般默认不需要手动处理框架里面已经针对模块系统做了适配。但如果你在自研框架里用CGLIB又跑在JDK 17及以上遇到这类异常优先检查启动参数和依赖版本。6.5 调试秘籍把代理类和字节码落地反编译动态代理最让人头疼的地方是看不见。调查问题时强烈建议把生成的代理类或字节码落盘然后反编译看结构。JDK动态代理设置jdk.proxy.ProxyGenerator.saveGeneratedFilestrueCGLIB设置cglib.debugLocation/你的路径都能让JVM输出产物。我去年排查过一个极其诡异的场景某个Service在AOP增强后日志输出比预期多了一次。把代理类反编译出来后发现是切点定义穿透了同一个类被代理了两层代理对象嵌套代理对象。虽然最终通过调整切点表达式解决了但反编译代理类提供的证据让排查时间从一天缩短到了两小时。这个手段的价值在复杂的代理叠加场景里怎么强调都不过分。6.6 一个特殊的坑代理对象与equals/hashCode动态生成的代理对象除了业务接口方法外还继承了Object的方法。JDK动态代理默认会转发equals、hashCode、toString这些方法给InvocationHandler处理。如果你在invoke中没有对Object方法做特殊判断可能会导致集合操作、对象比较出现意外结果。经验做法是在invoke开头对Object方法做短路处理当method.getDeclaringClass()是Object.class时直接调用method.invoke(this, args)不要向后转发。这样代理对象自身的行为保持正常不会因为增强逻辑干扰到equals等基础方法。前面MyBatis示例中已经写了这一行实际开发中它是一种很值得推广的习惯。我个人在实际项目里踩过的坑总结下来其实就一句话动态代理不是魔法它只是在一个不被注意的夹层里替你完成了方法调用的路由和包装。理解了这个夹层的存在很多看似玄学的问题比如事务不生效、日志重复打印、类型转换失败都能顺着代理对象是谁、它是为谁生成的、方法调用有没有经过它这三个问题层层剥开。也希望这块内容能帮你在面试里把话说到点子上——毕竟面试官听到你能把$Proxy0的结构和FastClass的调用索引都讲出来和你只说一句JDK代理接口CGLIB代理类完全是两个档次的评价。