资讯动态

Java动态代理从原理到实战:JDK与CGLIB全解析

发布时间:2026/10/20 23:23:00 来源:尧图企业网站定制
动态代理这个东西在Java后端开发里几乎无处不在。你用的AOP切面、声明式事务、RPC调用的服务治理底层基本都离不开动态代理。我记得第一次在面试里被问到动态代理是什么的时候只憋出来一句运行时生成代理对象然后就没了下文。后来真去啃源码、动手写了好几版Demo才算是把这块硬骨头啃明白。这篇文章我会从代理模式的最基本概念讲起把JDK动态代理和CGLIB动态代理两条路线都拆开讲清楚原理、写完整代码示例、复盘实际开发里踩过的坑。无论你是准备面试的Java新人还是想真正搞懂框架细节的开发者这篇应该都能给你一些干货。1. 动态代理到底是什么先搞清楚它解决了什么问题1.1 从静态代理说起代理模式本身是个设计模式核心思想是不直接操作目标对象而是通过一个代理对象去间接访问在访问前后可以插入其他逻辑。比如租房子你直接找房东是直连但很多事你不想或者不方便直接对房东说于是有了中介。中介负责带看、谈价格、签合同甚至替你把关风险但真正住的人和房东的交易本身不变中介就是代理对象。在Java里最简单的静态代理长这样假设有个接口 UserService有个实现类 UserServiceImpl现在你想在 getUserName 方法执行前后打印日志但不改动原来的实现类。最朴素的写法就是写一个 UserServiceProxy 类也实现 UserService 接口内部持有 UserServiceImpl 引用然后每个方法都转发调用转发前后加点自己的逻辑。public interface UserService { String getUserName(String userId); } public class UserServiceImpl implements UserService { Override public String getUserName(String userId) { // 模拟查询数据库 return 用户- userId; } } public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public String getUserName(String userId) { long start System.currentTimeMillis(); // 前置逻辑日志、权限校验等都可以放这里 String result target.getUserName(userId); long cost System.currentTimeMillis() - start; System.out.println(getUserName 耗时: cost ms); return result; } }这种写法的缺点很直观。第一一个接口就需要一个代理类方法越多代理类里需要手写的转发代码越多第二如果目标类没有接口静态代理这条路基本走不通第三一旦你要加新的横切逻辑比如把日志换成权限校验所有代理类都要跟着改。1.2 动态代理的核心价值动态代理把上面这些问题一次解决了。它的本质是在运行时动态生成一个代理类这个类不是我们手写的而是JVM根据你给的接口或父类临时造出来的。你只需要提供一个拦截处理逻辑至于代理类的生成、方法调用的转发都由框架完成。这样带来的好处是代理类不再需要手写一个拦截处理器就能服务所有目标对象。横切逻辑集中在一个地方改一处全局生效。可以在运行时动态决定要不要代理、代理谁灵活度完全不在一个量级。说白了静态代理是写死的代理动态代理是算出来的代理。理解了这点后面看JDK和CGLIB的两条实现路线就顺了。2. JDK动态代理基于接口的实现方案2.1 三个核心角色JDK动态代理是官方自带的方案不需要引入任何第三方依赖核心就两个类Proxy 和 InvocationHandler。InvocationHandler一个接口里面只有一个 invoke 方法。你写的拦截逻辑全部放到这个方法的实现里。ProxyJDK提供的一个工具类核心方法是 newProxyInstance用来生成代理实例。目标对象被代理的那个真实对象一般通过构造器传入 InvocationHandler 实现类或者通过其他方式关联起来。invoke 方法的长相Object invoke(Object proxy, Method method, Object[] args) throws Throwable三个参数分别是代理对象本身、当前被调用的方法、方法参数。每次外部通过代理对象调用任何方法都会进到这个 invoke 里来。你在里面可以做前置处理然后调用 method.invoke(target, args)再做后置处理。2.2 完整的代码示例继续用前面 UserService 的例子改造成JDK动态代理public 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()); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println( 方法结束, 耗时: cost ms); return result; } }生成代理对象并调用UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.getUserName(10001);控制台输出 调用方法: getUserName 方法结束, 耗时: 1ms这里有几个容易出错的点我第一次写都踩过。第一newProxyInstance 的第二个参数传的是接口数组不是目标类的所有类。如果目标实现了多个接口这里要把所有需要代理的接口都列出来只列一个那么代理对象只能当这一个接口的实例用。第二第三个参数 InvocationHandler 实例是全局共享的一个 handler 实例可以服务多个代理对象只要内部别把 target 绑死就行。第三proxy.getUserName 调用的是代理对象不是 target 对象。如果你不小心在外部直接对 target 调用那自然不会进 invoke这也是很多人混淆代理和原对象的常见原因。2.3 底层原理代理类是怎么来的JDK动态代理的底层机制一句话总结Proxy.newProxyInstance 会先在本地的 WeakHashMap 里查缓存缓存里没有就生成一个名为 $Proxy0 的代理类字节码用指定的 ClassLoader 加载反射创建实例。生成的 $Proxy0 有几个特征它继承自 java.lang.reflect.Proxy。它实现了你传入的所有接口。它对接口里的每一个方法都实现了这样一段逻辑直接调用 InvocationHandler.invoke(this, method, args)。因为是继承 Proxy而 Java 只能单继承所以JDK动态代理只能面向接口工作。如果你的目标类没有实现任何接口JDK方案就直接没戏。这不是设计缺陷而是先天约束。早期很多人抱怨这一点才催生了CGLIB这条基于继承的路线。还有一个有趣的细节$Proxy0 对 Object 类的 equals、hashCode、toString 三个方法也做了特殊转发处理不用像普通方法那样直接走 invoke。这个设计主要是为了保证代理对象作为集合元素时行为正常。比如你往一个 HashMap 里放代理对象它的 hashCode 和 equals 能按照目标对象来处理不会因为代理本身是个新类就出现元素查不到的问题。3. CGLIB动态代理基于继承的实现方案3.1 CGLIB的机制CGLIB 的全称是 Code Generation Library底层依赖 ASM 这个字节码操作框架。它的思路和JDK完全相反不去代理接口而是直接生成目标类的一个子类在子类里重写非 final 的方法把方法调用拦截下来转发到 MethodInterceptor 的 intercept 方法里。由于是生成子类这套逻辑CGLIB 不要求目标实现接口类本身也可以没有接口。但与之对应的final 类无法被继承final 方法无法被重写这两类对象CGLIB是代理不了的。如果要引入CGLIBMaven依赖加上即可dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency3.2 MethodInterceptor的使用CGLIB 的核心有两个类Enhancer增强器和 MethodInterceptor方法拦截器。Enhancer 负责配置父类、拦截器最后创建代理对象MethodInterceptor 的 intercept 方法负责接收调用public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwableobj增强后的代理对象。method被调用的原始方法。args方法参数。proxyMethodProxy内部封装了对原始方法的索引加速调用通常用 proxy.invokeSuper(obj, args) 来调用父类即目标类方法。代码示例public class LogInterceptor implements MethodInterceptor { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { long start System.currentTimeMillis(); System.out.println( 调用方法: method.getName()); Object result proxy.invokeSuper(obj, args); long cost System.currentTimeMillis() - start; System.out.println( 方法结束, 耗时: cost ms); return result; } }创建代理对象Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserServiceImpl.class); enhancer.setCallback(new LogInterceptor()); UserServiceImpl proxy (UserServiceImpl) enhancer.create(); proxy.getUserName(10001);注意这里我可以直接强转成 UserServiceImpl因为生成的代理对象就是 UserServiceImpl 的子类继承关系在那儿摆着。这也是CGLIB一个很舒服的地方没有接口也能代理调用方完全无感知。3.3 JDK动态代理与CGLIB的对比把两条路线放在一起看对比维度JDK动态代理CGLIB动态代理代理生成方式运行时生成实现接口的代理类运行时生成目标类的子类是否依赖接口必须依赖接口不要求接口对final类的支持不涉及基于接口不支持final类对final方法的支持不涉及不支持无法重写第三方依赖JDK自带零依赖需要cglib和asm依赖调用机制通过Method.invoke反射调用通过MethodProxy索引加速调用选型时我的原则很简单能用接口就用JDK代理毕竟零依赖、官方维护、排查问题也容易目标类确实没有接口再上CGLIB。网上很多CGLIB一定比JDK快的说法在JDK 8之后其实没那么绝对了反射调用这块JVM做了大量优化两者差距在业务方法本身耗时面前通常可以忽略。真正影响性能的反而是代理类的生成时机、缓存策略这类因素。4. 动态代理的典型应用场景4.1 AOP切面日志和性能监控动态代理最典型的应用就是AOP。所谓切面就是把日志、权限校验、埋点监控这些横切逻辑从业务方法里抽出来在代理层统一处理。比如一个支付服务我有几十个方法都需要记录入参、出参、耗时。如果用静态代理每个方法都要手动写一遍调用前后的逻辑工作量巨大还容易漏。用动态代理写一个 LogInterceptor 挂上去所有方法自动生效。新增业务方法时切面逻辑天然覆盖不需要额外改动。我实际维护的某个模拟项目中就是这样给一批历史遗留的查询接口统一加了慢查询日志。几十个方法改完全部业务类的时间成本几乎为零只动了代理配置那一处。这就是横切逻辑集中化的威力。4.2 事务管理与延迟加载声明式事务是动态代理另一个经典场景。框架在代理层拦截到方法调用后判断方法是否需要事务需要就先开启事务再调用目标方法目标方法抛异常就回滚事务正常返回就提交事务。你写的业务代码里完全看不到事务细节但事务确实生效了。这就是事务声明式而不编程式的原因。延迟加载也常借助代理实现。比如一个订单对象里有个 list 属性数据量很大直接查出来会拖慢主查询。那就可以用一个代理对象占位第一次真正访问这个属性时代理才触发真实的数据加载。用户无感知性能实实在在提升了。4.3 框架底层的应用逻辑很多主流框架的底层都有动态代理的身影。比如RPC框架里客户端拿到的往往是一个代理对象你调用它的方法时代理负责把方法名、参数序列化成网络请求发到服务端再把返回结果反序列化回来。整个过程从调用方的视角看和调用本地方法没什么区别。再比如一些ORM框架里通过代理拦截方法调用可以让查询结果对象在使用时才真正执行查询这也是延迟加载的一种实现。可以这样说凡是需要在不改源码的前提下给现有方法加逻辑、或者把一个远程/延迟操作伪装成本地调用的场景动态代理都是绕不开的方案。5. 实战中的坑与排查技巧5.1 常见问题速查表现象可能原因解决办法ClassCastException: $Proxy0 cannot be cast to UserServiceImplJDK代理生成的类继承Proxy和实现类没有继承关系只能强转为接口类型方法没有进入拦截器调用的是目标对象而非代理对象确保所有外部调用都持有代理引用final方法代理不生效CGLIB无法重写final方法去掉final修饰或改用其他方案代理对象内部方法互相调用时切面失效目标方法内部this调用没有经过代理改用代理对象调用或通过AopContext拿到当前代理类加载器相关异常接口和目标类可能由不同ClassLoader加载确认传入的ClassLoader能加载所有相关接口第一条是新手最常见的问题。假设写代码时看到 target 实现了 UserService就想当然地把代理对象强转成 UserServiceImpl结果一运行就抛 ClassCastException。原因就是JDK代理生成的 $Proxy0 是 Proxy 的子类、UserService 接口的实现类但和 UserServiceImpl 没有任何父子关系。这个错误我见过太多人犯了包括我自己早期也中过招。记住JDK代理生成的对象永远只能以接口类型去引用。5.2 自调用问题自调用问题在实际开发里非常隐蔽。假设你在 UserServiceImpl 里写了一个对外方法 saveUsersaveUser 内部调用了本类的另一个方法 validateUser。你通过代理对象调用 saveUser 时saveUser 的代理逻辑正常生效但当 saveUser 内部执行到 this.validateUser() 时这个 this 是目标对象不是代理对象所以 validateUser 上的切面逻辑根本没机会执行。我在一个真实的项目里就遇到过一次接口入参校验的切面在该方法被外部调用时正常拦截但被同类其他方法内部调用时完全无效排查了很久才定位到是自调用问题。解决方式通常有三种把内部方法也拆到另一个类里通过代理去调用。把内部方法改成由外部代理对象调用。使用框架提供的AopContext工具在方法内部取出当前代理对象再调用。第一种方式最干净也是我推荐的首选。5.3 性能实测与选型建议关于性能我简单聊一下实测感受。在一个模拟业务场景里我对一个只有一行计算逻辑的方法分别用JDK动态代理和CGLIB做了调用压测连续调用一百万次两者的总耗时差距大概在个位数毫秒到几十毫秒之间。但同样的方法如果内部有一次数据库查询网络耗时可能就是几百毫秒代理层的这点开销完全可以忽略。所以选型上业务代码优先考虑能不能满足需求而不是谁更快目标有接口优先JDK动态代理零依赖、好调试。目标没有接口再考虑CGLIB。如果存在大量代理对象的创建场景注意代理类的缓存机制尽量避免重复创建同一个类的代理。还有一个细节CGLIB创建代理对象时会生成新类这个生成过程是有固定开销的。如果你的应用在启动阶段创建大量代理启动时间会变长如果有条件尽量在启动时提前初始化避免运行期边请求边生成。5.4 一些实操心得最后分享几个自己积累的小经验。第一写InvocationHandler时不要在里面做太多重量级操作。invoke 是每次调用的必经之路你在里面加一次序列化、一次额外IO等于给每个业务方法都加了一份无法绕过的开销。第二调试动态代理时可以在IDE里给代理类打断点但更高效的方式是在 InvocationHandler 里直接打印方法名和参数确认拦截范围。很多人定位不到问题其实是连代理到底有没有生效都没确认。第三如果项目引入了Spring大部分动态代理的需求都已经被框架的抽象层覆盖了优先复用框架的AOP能力而不是自己再造一套Proxy工具。自己写的时候反而要注意和框架代理的叠加避免出现双代理、逻辑重复的问题。动态代理这块内容从面试背概念到真正手写会用中间差的恰恰是那些不起眼的细节。希望这篇整理能帮你把这条路的距离缩短一点。如果读到这里你手边有代码环境强烈建议把文中的两个Demo自己敲一遍再跑一跑我说的自调用那个反例感受会完全不一样。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑