
静态代理和动态代理这个话题网上文章一抓一大把但大部分都停在“动态代理看起来好牛逼”的层面。真到面试官问你“JDK动态代理生成的代理类长什么样为什么它只能代理接口CGLIB和JDK代理到底差在哪”的时候很多人还是会卡住。这篇二里我干脆把两个东西掰开揉碎从字节码层面看到调用链再结合Spring AOP里最容易踩的坑一起讲清楚。内容不搞虚的有示例、有源码、有反编译验证也有我实际调式时撞过的墙。适合对代理模式有基础了解、想彻底弄懂底层机制并用到项目里的人。1. 静态代理真的不行了吗——先看清它的问题到底在哪1.1 一个最基础的静态代理示例静态代理的思路其实非常简单定义一个和真实目标对象相同的接口代理类实现这个接口内部持有真实对象然后在每个方法前后插入增强逻辑。我先写一个很常见的用户服务例子看起来长这样public interface UserService { void saveUser(String name); }public class UserServiceImpl implements UserService { Override public void saveUser(String name) { System.out.println(保存用户 name); } }现在要给它加日志和事务控制最直接的方式就是写一个代理类public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public void saveUser(String name) { System.out.println(事务开始...); try { target.saveUser(name); System.out.println(事务提交...); } catch (Exception e) { System.out.println(事务回滚...); throw e; } } }调用的时候把真实对象包进代理类UserService userService new UserServiceProxy(new UserServiceImpl()); userService.saveUser(张三);这段代码本身没什么问题组合模式加接口实现结构清楚跑起来也稳定。静态代理最大的特点就是“简单、直观”代理类、目标类、接口都在编译期写死了IDE里点一下就能跳转出了问题不用猜。很多新手项目一开始就是从这种写法起步的它完全能跑也不会出什么幺蛾子。1.2 静态代理的代价类爆炸、逻辑重复、维护成本高静态代理的问题不是出在“能不能用”而是出在“扩展一个横切逻辑时要改多少地方”。假设你现在有用户服务、订单服务、库存服务三个接口都想加上日志和事务。如果继续用静态代理就得写三个代理类每个类里的日志代码和事务代码几乎一模一样只是目标对象不同。我见过一个真实项目早期为了给几十个Service加上统一的权限校验写了三十多个静态代理类。后来产品要改校验逻辑开发人员加班改了一整晚原因就是这些代理类里的重复代码散落得到处都是。这就是典型的“类爆炸”和“逻辑重复”每增加一个被代理对象就要新建一个代理类每修改一次增强逻辑就要改动所有代理类。更深一层看静态代理是把“增强逻辑”硬编码到了类的结构里。代理类一旦编译完它跟目标对象的绑定关系就固定了。你用组合方式虽然可以把目标对象换成任意实现类但代理类本身仍然只能服务“同一个接口”。除非你为每一种接口都单独写一个代理类否则这个增强逻辑没法复用到另一个完全不同业务接口上。所以静态代理并不是“不行了”而是它更适合逻辑稳定、接口变化少的场景。如果你只是想给某个接口加一层访问控制静态代理反而比动态代理更直观。但一旦增强逻辑开始横跨多个业务对象静态代理的维护成本就会指数级上升这时候就该考虑动态代理了。2. 动态代理的本质把增强逻辑从代理类中抽出来2.1 一个Handler搞定所有类的日志动态代理和静态代理相比最大的区别在于代理类不是写死的而是在运行时由JVM动态生成的你只需要提供一个“增强逻辑”处理器。还是刚才那个日志场景用JDK动态代理可以这样写import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; 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(before method.getName()); Object result method.invoke(target, args); System.out.println(after method.getName()); return result; } }然后创建代理对象import java.lang.reflect.Proxy; UserService userService (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogHandler(new UserServiceImpl()) ); userService.saveUser(张三);注意这个LogHandler完全没有依赖UserService接口它接受任何对象作为目标。今天你想代理用户服务明天想代理订单服务只需要换一下目标对象和接口数组就行增强逻辑写一份就到处能用。这就是动态代理的核心价值把“代理类”从静态写死变成运行时生成把“增强逻辑”从代理类中抽出来放到一个独立处理器里。你的业务代码不需要每个接口都写一个代理类只需要写一个通用的处理器然后告诉框架“我想给谁加什么逻辑”就够了。2.2 JDK代理和CGLIB代理怎么选一张表说清凡是用过动态代理的人都会遇到一对经典组合JDK动态代理和CGLIB动态代理。很多人只记住了“JDK代理要接口CGLIB不用接口”但真正选型的时候还得看更多差异。我把两者的核心对比整理成一张表对比维度JDK动态代理CGLIB动态代理实现原理运行时生成一个实现目标接口的代理类运行时生成目标类的子类目标要求必须存在接口不需要接口普通类即可生成类名称通常类似$Proxy0目标类$$EnhancerByCGLIB$$字节码生成基于标准Java反射和ProxyGenerator基于ASM字节码操作库可代理方法接口中定义的方法非final且可被继承重写的方法构造方法只能通过接口访问会调用父类的构造方法Spring中的默认选择Spring Boot 1.x之前的常见默认Spring Boot 2.x默认开启proxyTargetClass性能特点创建代理对象快反射调用有一定开销代理对象创建稍慢但后续调用使用FastClass索引通常更快这张表里最重要的信息不是“快和慢”而是“代理类到底是怎么诞生的”。JDK代理是在运行时生成一个实现了指定接口的类所以它天然依赖接口。CGLIB是生成目标类的子类通过重写父类方法实现增强所以不依赖接口但恰恰因为依赖继承final方法和final类就成了它的死穴。2.3 为什么JDK动态代理只能代理接口而不是类很多同学第一次用JDK动态代理时会试着直接传一个类进去UserServiceImpl userService (UserServiceImpl) Proxy.newProxyInstance(...);结果一跑就抛ClassCastException甚至IllegalArgumentException。原因很简单JDK生成的代理类默认继承了一个Proxy基类Java是单继承所以这个新生成的类不可能再去继承你的UserServiceImpl它唯一能做的就是实现接口来扩展能力。接口在这里起的作用是“给代理类一个合法的类型身份”。你调用Proxy.newProxyInstance时传入的接口数组决定了生成的代理类长什么样、能强转成什么类型。如果目标类没有实现任何接口代理类就完全没有办法和目标类建立方法签名层面的对应关系自然也就无法代理了。这就是JDK动态代理的边界。理解了这个边界你再回头看Spring里的proxyTargetClass配置就会很清楚Spring默认用JDK代理但如果你的Bean没有接口或者你强制要求用类代理Spring就会切换到CGLIB因为CGLIB绕开了“必须继承Proxy”的这个限制。3. JDK动态代理源码级剖析3.1 一段标准的JDK Proxy代码我们先写一个标准JDK动态代理的完整代码方便后面讲源码时对照。假设要给一个计算器接口加耗时统计public interface Calculator { int add(int a, int b); }public class CalculatorImpl implements Calculator { Override public int add(int a, int b) { return a b; } }处理器public class TimingHandler implements InvocationHandler { private final Object target; public TimingHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(method.getName() cost cost ms); return result; } }创建代理Calculator calculator (Calculator) Proxy.newProxyInstance( Calculator.class.getClassLoader(), new Class[]{Calculator.class}, new TimingHandler(new CalculatorImpl()) ); int result calculator.add(1, 2); System.out.println(result);跑完之后你会发现在TimingHandler里能拿到被调用方法的Method对象、入参数组并且在方法执行前后做增强。这个模式很眼熟对吧Spring里的Around通知本质上也是类似的套路只是它帮你把切面表达得更优雅了。3.2 newProxyInstance内部做了什么从ProxyFactory到字节码Proxy.newProxyInstance看起来只是一个工厂方法但它背后做了很多事情。我简化了一下关键流程第一步校验传入的接口是否合法。比如接口不能重复、接口必须是接口类型、接口不能是java.lang.Object的方法等。目标类有没有实现接口其实无所谓它只关心你传进接口数组里的是不是真正的接口。第二步调用getProxyClass0这里有一个很重要的缓存机制。如果同一个类加载器和同一个接口组合已经被创建过那么直接返回缓存的代理类避免重复生成字节码。第三步通过ProxyClassFactory生成代理类的字节码。JDK底层使用ProxyGenerator.generateProxyClass它会根据接口里的方法、Object的equals、hashCode、toString等方法生成一个完整的class文件。这个class文件包含一个构造方法构造方法接收InvocationHandler参数并把它赋值给继承自Proxy的h字段。第四步通过反射拿到这个构造方法并实例化代理对象传入你自定义的InvocationHandler实例。整个流程可以用一句话概括JDK动态代理不是直接用反射拦截你的方法而是先“写”一个类把方法调用统一转发给h.invoke()然后你在invoke()里做增强。3.3 invoke方法里三个参数的实战用法invoke(Object proxy, Method method, Object[] args)这三个参数很多人只用了method和args却忽略了proxy还经常在proxy上踩坑。proxy就是当前生成的代理对象本身。它最大的用处是作为“代理标识”比如你想区分当前调用是来自代理还是来自具体目标或者需要在回调里把代理对象传给某个框架时使用。但它也是最容易引发递归的地方。你要是手一抖在invoke里写了proxy.toString();那就完了。因为toString()又会触发代理类的调用再次进入invoke无限递归直接栈溢出。这一点我在给同事review代码时见过好几次排查起来特别痛苦因为报错堆栈只是巨大的StackOverflowError根本看不出第一行是谁触发的。method参数是反射得到的Method对象你可以拿到方法名、参数列表、注解信息。想在增强逻辑里根据方法名做差异化处理通常就是靠它。比如只给名字以save开头的方法加事务而不是所有方法都加。args就是方法入参数组。注意如果方法是无参的这里得到的是null而不是空数组。所以你遍历args之前一定要判断一下args ! null否则分分钟NullPointerException。3.4 把生成的$Proxy0反编译出来看看光看源码描述还是有点抽象最好的方式是把JDK生成的代理类保存到本地然后反编译。以前我调试代理逻辑时会在代码里加这么一段import sun.misc.ProxyGenerator; import java.io.FileOutputStream; byte[] bytes ProxyGenerator.generateProxyClass($Proxy0, new Class[]{Calculator.class}); try (FileOutputStream out new FileOutputStream(Proxy0.class)) { out.write(bytes); }然后打开终端用javap -c Proxy0查看字节码。你会看到类似这样的关键片段代理类的add方法里面并不是直接调用CalculatorImpl.add而是调用了super.h.invoke(this, method, args)。也就是说每一次方法调用都变成了对InvocationHandler.invoke的调用而真正的方法是你在invoke内部通过反射再去触发的。这里顺便说一句在JDK 9之后sun.misc.ProxyGenerator已经不允许直接访问了模块化之后包名也有变化。不过你依然可以通过-Djava.base之类的方式做实验或者干脆用上面这段代码在JDK 8环境验证一次再回到高版本看行为。理解了这个过程后JDK动态代理的底层对你来说就不再是黑盒了。4. CGLIB动态代理的完整面貌4.1 示例Enhancer MethodInterceptorCGLIB的使用方式和JDK代理不太一样。它不需要接口直接对一个普通类做增强。我们用刚才的CalculatorImpl这个类但这次不让它实现任何接口只是一个普通类public class CalculatorImpl { public int add(int a, int b) { return a b; } }CGLIB创建一个代理import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; Enhancer enhancer new Enhancer(); enhancer.setSuperclass(CalculatorImpl.class); enhancer.setCallback(new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { long start System.currentTimeMillis(); Object result proxy.invokeSuper(obj, args); long cost System.currentTimeMillis() - start; System.out.println(method.getName() cost cost ms); return result; } }); CalculatorImpl proxy (CalculatorImpl) enhancer.create(); int result proxy.add(1, 2); System.out.println(result);注意这里我使用的是proxy.invokeSuper(obj, args)不是method.invoke(obj, args)。很多人会写错成后者也能跑但性能就差很多了。invokeSuper走的是CGLIB的FastClass机制它通过索引直接定位到父类方法省去了反射查找的时间而method.invoke走的是标准的反射调用。4.2 底层原理子类继承与FastClass索引CGLIB生成出来的代理类本质上是目标类的一个子类。JVM在运行时通过ASM库动态生成一个子类字节码这个子类把所有可重写的方法都重写了一遍并在每个方法体里调用你设置的MethodInterceptor.intercept()方法。但CGLIB不是直接使用反射去调用目标方法而是额外生成了两个FastClass类一个用于调用目标对象的方法一个用于调用代理对象的方法。FastClass里边维护了一个方法签名到整数索引的映射调用时直接根据方法签名找到索引再通过索引跳转到对应的方法体少掉了反射方法查找的开销。这也是为什么很多性能对比文章会说CGLIB调用比JDK代理快。但在JDK 8以后随着JIT对反射的优化越来越激进两者的实际差距已经小很多了。真正决定你选JDK代理还是CGLIB的往往不是性能而是“目标对象有没有接口”和“你有没有强制使用类代理”。4.3 CGLIB的限制final方法、static方法、final类都不能被代理CGLIB能代理普通类但它依赖的是“生成子类并重写父类方法”。一旦方法被final修饰子类就无法重写类被final修饰子类根本无法生成。同样的static方法属于类本身不是实例方法也不会被子类重写所以CGLIB对static方法同样无能为力。我在项目里遇到过这样一个案例有个老代码里的工具类方法被定义成public static业务方想通过CGLIB给它加一层缓存结果怎么增强都不生效。后来追代码发现静态方法根本没有进入intercept因为这个方法压根不会被CGLIB生成到代理子类的方法表里。另外CGLIB还有一个容易被忽略的细节Enhancer在创建代理对象时会调用父类的构造方法。如果父类没有无参构造方法或者构造方法里做了重量级初始化你使用CGLIB时就得小心了可能还没开始增强就已经把副作用跑了一遍。4.4 Spring里的选择逻辑为什么SpringBoot默认CGLIBSpring AOP从很早起就同时支持JDK代理和CGLIB。早期Spring选择JDK代理作为默认方案只要目标Bean实现了接口就默认用JDK代理只有目标Bean没有接口时才用CGLIB。这样设计的考虑是JDK代理是标准API不引入额外依赖安全且稳定。但Spring Boot 2.x之后默认把proxyTargetClass改成了true。也就是说即使Bean实现了接口Spring也优先用CGLIB创建子类代理。为什么这么改一方面是很多开发者在用JDK代理时遇到“类型强转只能转接口不能转实现类”的麻烦另一方面是CGLIB现在的成熟度和稳定性已经足够好类代理在一些场景下更符合直觉。实际使用中你不需要自己改Spring的默认配置但你完全可以关注一个问题如果服务莫名其妙的代理失效先看一眼当前Bean到底用的是JDK代理还是CGLIB。用类名排查JDK代理的类名里通常有$ProxyCGLIB代理的类名里通常有$$EnhancerByCGLIB$$一眼就能分辨出来。5. 框架和业务里的动态代理实践5.1 Spring AOP中代理对象的识别与调试Spring AOP的代理对象对业务代码几乎是透明的但排查问题时你不一定能看出来自己拿到的到底是不是代理。最直接的方法是打印类的全限定名System.out.println(bean.getClass().getName());如果打印结果里含$Proxy或$$EnhancerByCGLIB$$说明这个Bean已经被代理了。另一个办法是使用Spring提供的工具类import org.springframework.aop.support.AopUtils; boolean isProxy AopUtils.isAopProxy(bean);如果isProxy返回true说明它已经是代理对象。还有一个细节AopUtils.getTargetClass(bean)能拿到代理背后的真实目标类。调试的时候这个方法特别有用你想知道切面到底作用在类上的哪个方法直接查目标类即可。我自己的习惯是当发现某个带Transactional的方法突然不生效时第一步永远都是先打印Bean的真实类型。因为一旦确定Bean本身没有被代理后面的排查方向就完全改变了。很多时候问题不是出在切面逻辑而是Bean根本没有被Spring的AOP机制接管。5.2 事务和异步注解失效的经典案例动态代理在Spring中最常见的应用是Transactional和Async。这两个注解看着简单背后却都是AOP代理在起作用。我见过太多的“注解不生效”案例最后的根因都指向同一个问题同类内部的this调用绕过了代理对象。比如这样一个ServiceService public class OrderService { Transactional public void createOrder(OrderDTO dto) { // 下单逻辑 this.sendNotify(dto); } Async public void sendNotify(OrderDTO dto) { // 异步通知逻辑 } }createOrder方法内部调用的是this.sendNotify这个this是当前业务对象本身不是代理对象。所以Async根本不会生效因为异步代理的拦截逻辑没有机会执行。解决思路有三种。第一种是把sendNotify拆到另一个独立的Spring Bean中通过注入调用。第二种是在当前类里注入自己Autowired private OrderService self;然后用self.sendNotify(dto)调用。第三种是启用AopContextEnableAspectJAutoProxy(exposeProxy true)然后通过((OrderService) AopContext.currentProxy()).sendNotify(dto)获取代理。但这会让代码可读性变差我建议项目早期就把“内部方法调用也要走代理”的规则定清楚否则后面全是坑。5.3 代理对象序列化、equals/hashCode的坑动态代理对象在序列化时很容易出问题。JDK代理对象内部持有InvocationHandler而handler里又持有目标对象目标对象可能再引用一堆资源。你如果直接把代理对象丢给Redis或者写入消息队列很可能把一整条依赖链都序列化进去轻则数据膨胀重则反序列化失败。CGLIB代理对象也有类似的问题。代理类是动态生成的子类很多序列化框架对它并不友好甚至干脆序列化不了。我的建议是往缓存、MQ、数据库里存的永远是纯数据对象不要存代理对象。如果在接口返回层遇到Jackson序列化代理对象最好用JsonIgnoreType标记掉某些代理类或者在配置里关闭对代理的序列化。再说equals和hashCode。JDK代理对这两个方法的调用也会进入InvocationHandler.invoke。如果你的handler里没有专门处理它们那么把两个代理对象放进Set或者作为Map的Key时行为可能不符合预期。CGLIB代理默认会对equals和hashCode做处理但如果你希望基于目标对象的值去比较还是需要在callback里显式控制。总之尽量不要依赖代理对象做身份判断业务上要用的标识字段单独提取出来比什么都稳。6. 动态代理常见问题排查实录速查表6.1 五个方法判断当前对象是不是代理对象排查动态代理问题第一步永远是判断“我现在拿到的到底是不是代理对象”。我整理了五个快速判断的方法按使用频率排序如下打印bean.getClass().getName()类名含$Proxy多半是JDK代理含$$EnhancerByCGLIB$$多半是CGLIB代理使用org.springframework.aop.support.AopUtils.isAopProxy(bean)Spring项目里通用判断bean instanceof java.lang.reflect.Proxy只能识别JDK代理判断bean instanceof net.sf.cglib.proxy.FactoryCGLIB生成的代理类实现了这个接口查看Spring配置里的proxyTargetClass和spring.aop.proxy-target-class预判当前项目用哪种代理。这五个方法单独用不一定准但组合起来基本能锁定结论。我实际排查时最常用第一个因为通过类名一眼能看出代理类型后面的反推思路瞬间就清晰了。6.2 常见异常/问题与解决对照表现象常见原因解决思路ClassCastException: $Proxy0 cannot be cast to XxxImpl使用JDK代理接口本身存在但代码里试图强转成实现类改为强转为接口类型或切换CGLIB代理StackOverflowError日志反复出现某个get方法在invoke或intercept里调用了代理对象的方法避免在处理器中调用proxy的方法改为操作目标对象Transactional不生效日志没有事务边界同类内部this调用绕过了代理拆分Bean、注入自身或使用AopContextAsync不生效同上另外可能是异步回调通过this调用了同类方法让异步逻辑走代理对象CGLIB代理中final方法始终没被增强CGLIB通过子类重写实现final方法无法重写去掉final修饰或改用接口JDK代理Exception: Failed to instantiate ... no constructorCGLIB创建代理对象时调用了目标类构造方法但没有合适的构造器为目标类提供可访问的构造方法避免无参构造缺失序列化后丢失业务逻辑直接把代理对象存入缓存/消息体转换为普通DTO后保存不存代理对象方法调用性能明显偏低intercept里用了method.invoke而非methodProxy.invokeSuper改为CGLIB的FastClass调用方式表格之外还要提醒一句动态代理的很多“诡异问题”根源其实在“代理链叠加”。比如你用一个代理对象去创建另一个代理对象handler层层嵌套调用链又长又难查。遇到这类问题先把代理层数降到最小逐层验证会比看一堆堆栈高效得多。6.3 一套排查动态代理问题的“信号-路径”思路多年的调式经验告诉我排查动态代理问题不要上来就翻源码先想清楚四个关键信号当前Bean是不是代理、是哪种代理、方法是不是可被代理、代理逻辑有没有被触发。第一确认Bean是否被代理用上面那五个方法判断。第二确认代理类型JDK代理还是CGLIB代理这决定了你能代理的范围。第三检查目标方法的修饰符接口方法finalstatic这些都能直接决定代理能否生效。第四在handler里加一行输出确认拦截方法是否真的被执行。把这个“信号-路径”思想理清楚之后你会发现自己排查代理问题的速度会快很多。因为你不再漫无目的地在堆栈里翻而是按顺序逐个排除最后总能定位到具体环节。7. 实战中的几个经验与心得7.1 能用静态代理就用静态代理别为了炫技上动态代理这句话听起来有点反直觉但我想说的是技术选型永远跟着业务复杂度走。如果一个模块里只有两个接口需要加日志静态代理写起来十行代码逻辑清晰出了问题断点一打就到。强行换成动态代理反而引入了一堆“运行时生成类”的抽象排查问题的人如果没有相关经验看代码会懵半天。我个人的标准很简单增强逻辑只服务于一个或两个固定接口且接口本身变化不大就用静态代理如果增强逻辑要横跨多个不相干的业务对象或者将来可能要动态配置就上动态代理。静态和动态不是进化关系而是两种不同侧重的工具。7.2 增强逻辑如果可以配置handler和callback要设计好动态代理最强大的地方是“一个处理器可以服务所有目标对象”但它也很容易被写成一个上帝类职责全部堆到一个invoke或intercept方法里。我在实际项目里见过一个handler里面塞了日志、鉴权、租户隔离、敏感词过滤、数据权限十几种逻辑代码超过两千行改一个地方都要全局回归。更好的做法是把handler设计成小的、可组合的增强单元每个增强单元只做一件事再通过一个调用链把它们串起来。如果你需要运行时可配置还可以把这些增强单元的关键参数外置到配置中心。这样动态代理的灵活性才真正落地而不是变成新的技术债。7.3 一个调试技巧把生成的代理类保存下来肉眼反编译最后分享一个我用了很多年的调试技巧。动态代理的Bug往往难在“看不到代理类本身”你怀疑它有某段逻辑但代码里根本找不到这个类。把生成的代理类落盘再用javap反编译就能直接看清每个方法到底转交给了谁。JDK 8里可以用ProxyGenerator.generateProxyClass把代理class保存出来。CGLIB则可以通过Enhancer配合设置new WeakReference或者反射获取byte[]再写文件。反编译后你可能发现某些你以为会被拦截的方法根本没有进入handler或者invoke里调用proxy.xxx导致死循环。看懂这些字节码之后一切黑盒都会变成白盒。我个人在实际排查中最深的体会是静态代理和动态代理之间的本质区别从来不是“运行速度谁快谁慢”而是“增强逻辑到底写在编译期还是运行期”。静态代理把增强逻辑写死在类里看得见摸得着动态代理把增强逻辑抽出来在运行时把类拼出来。理解了这个底层思维再去看Spring AOP、Feign、MyBatis这些框架里的代理机制都会觉得特别顺。真要遇到难搞的代理问题别忘了先看一眼类名再决定从哪里下手。