Java代理模式全解析:静态代理、JDK动态代理与CGLib实战

发布时间:2026/10/3 9:57:55
Java代理模式全解析:静态代理、JDK动态代理与CGLib实战 做Java开发这些年代理模式是我见到的应用最广也最容易讲得云里雾里的设计模式之一。静态代理、JDK动态代理、CGLib动态代理这三个名词几乎出现在每一份面试题库里也藏在Spring AOP、MyBatis Mapper、Feign Client这些常用框架的底层。不少同事能背出概念但一到“为什么JDK代理必须接口”“为什么Spring Boot默认不用JDK代理”这种追问就会卡壳。这篇文章我会带着你从零手写一遍三种代理拆开它们背后的调用链再聊聊在实际项目里踩过的坑。我会尽量用大白话先讲清楚它到底要解决什么问题再去看代码和字节码文章里的每段代码都可以直接复制运行。1. 代理模式要解决的到底是什么问题1.1 从一段“没法改”的线上代码说起假设你接手了一个老项目里面有个OrderService写满了业务逻辑几十个方法直接操作数据库。某天领导要求给所有查询方法加耗时统计并且明确说“不能大改别动核心逻辑”。你打开OrderService一看每个方法里都有数据库连接、参数校验、异常处理硬改的话工作量巨大还可能改出线上事故。这时候你需要的是一种“在不改目标类源码的前提下在方法调用前后插入统一逻辑”的能力。代理模式就是为这种横切增强而生的。这种场景在真实项目里太常见了加日志、加事务、加权限校验、做接口幂等等其实都是横切逻辑。如果每个业务方法里都手写一遍System.currentTimeMillis()代码会变得极其啰嗦而且一旦需求变化所有方法都要跟着改。代理模式的核心作用就是把这些横切逻辑从业务代码里抽走让业务开发只关注业务本身。1.2 代理的基本结构中介者思维用最直白的话说代理模式就是“客户端不直接操作目标对象而是操作一个中间层”。这个中间层就是代理对象它内部持有目标对象的引用在目标方法被调用之前和之后执行额外的增强逻辑。生活里最常见的例子是房产中介房东是目标对象租客是客户端中介就是代理对象。租客找中介看房中介带看、介绍情况、签合同最后房东才出来收尾。租客不需要认识房东房东也不想被每个租客打扰中间的沟通和约束都由中介完成。代理模式里的四个角色也对应得很清楚抽象主题定义了“租房”这个能力真实主题是房东代理是中介客户端是租客。1.3 三种代理方式的关系总览Java里实现代理的方式虽然很多但核心思路一致静态代理是手写代理类编译器帮你生成代码JDK动态代理通过接口和反射在运行时生成代理类CGLib通过继承目标类生成子类再在子类里覆写方法。静态代理最直接但维护成本高JDK动态代理干净、规范但必须依赖接口CGLib能处理没有接口的类却要依赖字节码生成库。它们没有绝对的好坏只是适用场景不同。后续会用代码把三个都过一遍你就能直观感受到它们之间的巨大差异。2. 静态代理最简单也最容易暴露问题2.1 手写一个静态代理静态代理说白了就是自己写一个代理类实现和目标类相同的接口然后在代理类里调用目标类的方法加上增强逻辑。先定义一个接口和一个实现类public interface UserService { void save(String name); } public class UserServiceImpl implements UserService { Override public void save(String name) { System.out.println(保存用户 name); } }接着写代理类让它也实现UserService接口并持有真实目标对象public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public void save(String name) { System.out.println([日志] 开始保存参数 name); long start System.currentTimeMillis(); target.save(name); long cost System.currentTimeMillis() - start; System.out.println([日志] 保存结束耗时 cost ms); } }使用的时候只需要把真实对象塞给代理UserService service new UserServiceProxy(new UserServiceImpl()); service.save(张三);运行结果会多出两行日志而UserServiceImpl的代码一个字都没改。这就是静态代理最简单的形态。2.2 为什么静态代理不适合真实项目静态代理实现起来很直白但也很笨重。第一个问题是“类爆炸”一个UserService要写一个代理类一个OrderService又要写一个代理类项目中服务一多代理类数量直线上升。第二个问题是“接口变更灾难”如果UserService增加了一个delete方法接口、实现类、代理类三处都要同步修改漏改一个就编译不过。更关键的是静态代理的增强逻辑没法复用。日志、事务、权限这样通用的能力你写在UserServiceProxy里换个服务还得再写一遍本质上是把重复代码从业务类转移到了代理类。所以日常业务开发里我基本不建议手写静态代理它更适合用在场景固定、接口很少变的场景。2.3 静态代理的适用场景尽管静态代理有这么多毛病它并不是没有用。比如在接第三方SDK的时候SDK的接口方法固定且数量有限你只想对几个关键调用做埋点手写一个代理类反而是最可控的方式。另一个常见场景是做测试桩在单元测试里用一个代理对象替换真实的外部服务返回写死的数据这时候静态代理比Mock框架轻量得多。还有一种情况是目标类没有实现接口你也不想引入额外的动态代理库可以用继承实现静态代理写一个子类继承目标类重写需要增强的方法。这其实就是CGLib的雏形只不过是在编译期写死了。总之静态代理适合“少、稳、简单”的场景一旦要代理的对象变成动态集合就要果断换成动态代理。3. JDK动态代理基于接口的轻量级增强3.1 从Proxy.newProxyInstance说起JDK动态代理是Java标准库自带的代理方案从JDK 1.3就有了使用它不需要引入任何第三方依赖。核心入口是java.lang.reflect.Proxy的newProxyInstance方法方法签名是Proxy.newProxyInstance(ClassLoader loader, Class?[] interfaces, InvocationHandler h)三个参数分别代表类加载器、目标类要实现的接口数组、以及调用处理器。前两个参数负责“生成一个什么样的代理类”第三个参数负责“代理类的方法被调用时干什么”。生成代理类的关键是第二个参数interfaces。JDK动态代理只能围绕接口来生成代理类在运行时被Proxy强制继承并实现了这些接口。调用代理对象上的任意接口方法最终都会进入InvocationHandler.invoke方法由开发者决定是直接放行、加日志还是做权限校验。3.2 InvocationHandler里发生了什么InvocationHandler只有一个方法Object invoke(Object proxy, Method method, Object[] args) throws Throwableproxy是生成的代理对象本身method是当前被调用的接口方法args是方法参数。在这个方法里最常见的写法是通过method.invoke(target, args)反射调用真实目标对象并在前后加上增强逻辑。拿前面UserService的例子重写一遍动态代理版本是这样public class LogHandler implements InvocationHandler { private final Object target; public LogHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println([JDK动态代理] 调用前 method.getName()); long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println([JDK动态代理] 调用后耗时 cost ms); return result; } }创建代理的代码变成UserService service (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogHandler(new UserServiceImpl()) ); service.save(张三);这里有个容易忽略的点invoke方法里的method是从接口里反射出来的方法而method.invoke(target, args)调用的是真实对象上的实现。如果目标类重载了接口方法直接传target是没问题的因为method是UserService接口的方法UserServiceImpl一定实现了它。另一个很多人踩过的坑是在invoke里直接调用method.invoke(proxy, args)这样会陷入无限递归因为代理对象上再次调用同名方法又会进入invoke。正确做法是永远调用target除非你故意要做递归增强。3.3 为什么JDK代理必须要接口这是面试高频题也是理解JDK动态代理的钥匙。Proxy生成的代理类本身已经继承了java.lang.reflect.Proxy这个类。Java是单继承代理类不可能再继承你的目标类。那怎么让代理类和目标类对外表现一致呢只能靠接口代理类实现和目标类相同的接口客户端通过接口调用方法接口就是两者的共同契约。可以这样理解中介和房东本来毫无血缘关系他们能一起干活是因为都签了同一份“租房合同”接口。没有这份合同中介不能假装自己是房东租客也没有办法通过统一方式对待他们。所以JDK动态代理的前提是目标类必须实现至少一个接口否则压根没法生成代理类。3.4 项目中的实操给Service加日志和耗时统计实际项目里我们很少写一个通用的LogHandler给所有Service用但思想是一致的。比如你有多个Service接口想统计每个方法的耗时可以写一个工厂方法统一创建代理public static T T createProxy(T target, Class? targetInterface, InvocationHandler handler) { return (T) Proxy.newProxyInstance( targetInterface.getClassLoader(), new Class[]{targetInterface}, handler ); }然后把工厂方法封装成工具类在Spring配置或代码入口处把原始Bean替换成代理Bean。这样做的好处是业务代码完全无感知依然通过接口拿到对象但底层已经是增强后的代理实现。实际项目中还要注意线程安全InvocationHandler通常是单例的invoke方法会被多个线程并发调用里面的增强逻辑必须无状态或者使用线程安全的计数器。4. CGLib动态代理打破接口限制的字节码魔法4.1 CGLib生成子类的基本写法CGLib是一个字节码生成库它通过继承目标类在内存中生成目标类的子类并把子类伪装成目标类。所以CGLib不要求目标类实现接口但它要求目标类不能是final的。使用CGLib需要引入依赖如果用的是纯Java项目需要加dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency如果你在Spring框架里可以直接用org.springframework.cglib包下的类不需要额外引入。一个最简单的CGLib代理这样写public class UserManager { public void createUser(String name) { System.out.println(创建用户 name); } } public class UserManagerInterceptor implements MethodInterceptor { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println([CGLib] 调用前 method.getName()); Object result proxy.invokeSuper(obj, args); System.out.println([CGLib] 调用后 method.getName()); return result; } }创建代理Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserManager.class); enhancer.setCallback(new UserManagerInterceptor()); UserManager proxy (UserManager) enhancer.create(); proxy.createUser(张三);这里MethodProxy.invokeSuper很关键它不是反射调用而是直接调用父类即目标类的方法所以比反射性能更好。4.2 CGLib的增强逻辑与拦截器CGLib的MethodInterceptor.intercept方法参数比JDK动态代理多了一个MethodProxy。MethodProxy是CGLib根据父类方法生成的快速调用代理invokeSuper会走父类的原始方法invoke则有可能再次进入拦截器所以一般都用invokeSuper。CGLib还允许设置多个Callback通过CallbackFilter为不同方法配置不同增强逻辑。这在某些只希望拦截指定方法的场景里很实用。不过日常开发中用得最多还是单个MethodInterceptor因为Spring AOP已经帮我们封装好了很少直接操作Enhancer。如果你想控制代理对象的生命周期也可以让Enhancer实现BeanFactoryAware之类的接口但大多数情况下框架层会帮你处理好这些细节。真正要关心的是“CGLib生成子类”这个动作本身它会比JDK动态代理多消耗一些内存和类加载时间。4.3 CGLib有哪些限制CGLib不是万能的三大限制要记牢。第一目标类不能是final的。因为CGLib靠继承生成子类继承一个final类在Java语法层面就过不去。第二目标方法不能是final的。子类可以继承final方法但无法重写拦截器也就进不去。第三JDK高版本对字节码生成有模块限制。在JDK 9以上使用CGLib如果目标类不在java.base模块里可能需要额外加--add-opens参数否则会抛出类似InaccessibleObjectException的异常。Spring Boot里因为框架已经处理了大部分常见场景很少遇到但在命令行写Demo时就容易踩。还有一点CGLib生成的代理类是目标类的子类所以代理对象的类型和原始类型是“is-a”关系可以做强转。这一点和JDK动态代理恰好相反。5. 实战对比JDK vs CGLib以及Spring AOP中的选择5.1 关键维度对比表先把两种动态代理的核心差异列成一张表方便后面设计选型。对比项JDK动态代理CGLib动态代理原理运行时生成接口的匿名实现类运行时生成目标类的子类要求目标必须实现接口目标类不能是final方法增强通过InvocationHandler反射调用通过MethodProxy直接调用父类方法依赖JDK内置无额外依赖需要cglib库或Spring内部实现类型强转只能转成接口类型可以转成目标类类型性能JDK 8优化后接近CGLib创建代理时较慢调用较快final方法不受影响接口方法不能final无法增强final方法实际性能对比需要看JDK版本。JDK 8之后虚拟机对反射做了大量优化JDK动态代理的调用性能已经不输CGLib所以“CGLib一定更快”这个说法已经过时了。如果你正在设计一个通用框架建议优先考虑JDK动态代理因为零依赖、核心API稳定只有当目标是普通类或者你确实需要继承能力时再考虑CGLib。很多商业框架对CGLib包做了shade避免和用户依赖冲突这也是一个需要关注的点。5.2 Spring AOP默认选哪个Spring AOP默认的策略是如果目标对象实现了接口就用JDK动态代理如果目标对象没有实现接口就用CGLib。这个逻辑在DefaultAopProxyFactory里写得很清楚。不过从Spring Boot 2.x开始官方把spring.aop.proxy-target-class默认值改成了true意思是优先使用CGLib即使目标类有接口也会走CGLib。为什么要这么改因为直接用CGLib可以避免“只有接口却没有实现类”时JDK代理无能为力的边界情况也让AOP行为更一致。如果你想切回JDK动态代理在application.properties里设置spring.aop.proxy-target-classfalse这里要提醒一句不同Spring Boot版本的默认值有差异生产环境如果对代理方式有严格要求应该显式配置而不是依赖默认行为。5.3 自调用失效问题最容易被忽略的坑用Spring AOP时最经典的翻车案例是“自调用”。比如一个UserService内部方法调用另一个带缓存注解的方法Service public class UserService { Cacheable(user) public String getUser(String id) { return user- id; } public String getUserWithLog(String id) { return this.getUser(id); // 这里走的是this不是代理对象 } }表面上看getUserWithLog调用了getUser觉得Cacheable应该生效实际上调用的是目标对象本身的this.getUser根本没有经过代理对象缓存注解完全无效。这个问题和用JDK还是CGLib无关只要是基于代理的AOP都会有。解决方案有三种。第一是让两个方法拆到不同Bean注入对方的代理对象第二是在类内部注入自己比如使用Resource注入UserService但要注意循环依赖第三是配合AopContext.currentProxy()临时获取当前代理((UserService) AopContext.currentProxy()).getUser(id);使用第三种方案时需要设置exposeProxytrue在Spring Boot里可以配置EnableAspectJAutoProxy(exposeProxy true)。我个人更推荐第一种方案因为它最自然还不会暴露框架细节。6. 常见问题与排查技巧实录6.1 ClassCastException代理对象不能强转为目标实现类遇到这个异常第一反应先确认代理类型。JDK动态代理生成的代理对象只能强转成接口类型不能强转成目标实现类。比如UserService的一个实现是UserServiceImplProxy.newProxyInstance返回的对象在强制转成UserServiceImpl时会抛ClassCastException。这是因为代理类和UserServiceImpl没有继承关系只是兄弟关系。排查方法很简单看异常堆栈或者写个proxy.getClass()打印类名如果是com.sun.proxy.$Proxy0说明是JDK动态代理如果是xxx$$EnhancerByCGLIB$$xxx说明是CGLib代理。平时编码一律面向接口就能绕开这个问题。6.2 final类或final方法无法代理如果启动时报“cannot subclass final class”或者“final method cannot be overridden”说明目标类/方法不允许继承或重写。八成是你用CGLib代理了一个final类或者想增强final方法。这时候要么去掉final要么改成JDK动态代理要么手动在业务代码里插入逻辑。遇到这类问题先检查类上没有final修饰再检查方法没有final。6.3 代理对象比较为false的排查思路在缓存场景里有人喜欢用targetMap.containsKey(object)做判断结果发现代理对象和原始对象永远不相等。这其实不算Bug而是代理的本质决定的代理对象是JVM动态生成的新对象和原始目标对象必然不是同一个引用。排查思路是要么用接口的equals方法做业务比较要么在增强逻辑里取target做比较要么干脆别用对象引用做缓存Key。6.4 被代理对象里面的this调用为什么不会增强这个问题和自调用一脉相承。代理生效的前提是客户端持有的对象是代理对象但目标对象内部的this永远指向目标对象自身而不是代理对象。所以当methodA在内部调用methodB时methodB根本不会被增强除非methodA本身是被外部经过代理调用的。调试时可以在methodB里加一行Thread.currentThread().getStackTrace()或者临时打印this.getClass()看到类名没有代理标识就可以确认是自调用问题。还有一个类似的坑事务方法内部调用另一个事务方法如果都是this调用内层事务不会独立生效因为this调用不会经过事务代理。这是线上最容易出现的事务不生效原因之一排查时优先怀疑自调用。最后分享一个我自己摸索出来的排查习惯遇到任何代理相关的诡异问题先别猜代码逻辑先确认这个代理到底是JDK还是CGLib再确认目标对象有没有接口、有没有final修饰最后看调用链路上到底是代理对象还是原始对象。三步走下来90%的问题都能定位。代理模式并不难难的是记住“代理对象不是目标对象增强只发生在代理方法入口”把这句话刻在脑子里很多坑都会自动绕开。