尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

代理模式全解析:从静态代理到JDK动态代理、CGLIB与字节码增强

发布时间:2026/9/24 21:18:05

资讯中心
01
ARTICLE

代理模式全解析:从静态代理到JDK动态代理、CGLIB与字节码增强

代理模式全解析:从静态代理到JDK动态代理、CGLIB与字节码增强
只要有Java面试经验的读者应该都有感触代理模式几乎是一个绕不开的考点。从最基础的静态代理到JDK动态代理、CGLIB再到直接操作class字节码的ASM、Javassist这条技术脉络恰好串起了Java开发者从“会用框架”到“看懂框架”的完整进阶路径。很多人在面试八股文里背过“静态代理和动态代理的区别”但一旦面对Spring AOP源码、MyBatis的MapperProxy或者自定义注解做埋点这类实际问题还是容易一头雾水。这篇文章我会从代理模式要解决的原始问题讲起手写代码演示静态代理到JDK动态代理、CGLIB再往深处聊一聊字节码增强的实现思路最后整理一些面试高频考点和工程里的排坑经验。无论你是准备跳槽面试还是想把Spring AOP这类框架原理吃透这篇文章都能提供一个清晰的参考坐标。1. 代理模式到底在解决什么问题1.1 从“直接调用”到“间接调用”控制权的转移先想一个最基础的问题为什么非要加一层代理直接用目标对象不就行了假设你有一个UserService接口里面有个saveUser(User user)方法业务代码里直接new一个实现类调用方法整个过程清爽直接。但现实需求往往没那么简单——你可能要在方法执行前做权限校验在执行后记录日志在异常时做兜底处理。如果把这些逻辑塞进业务方法里业务代码会越来越脏而且多个业务方法之间会产生大量重复代码。代理模式的核心思路叫作“间接调用”不直接操作目标对象而是通过代理对象去访问目标对象。这层代理在中间充当了一个“守门员”或者“中转站”可以在调用真正业务逻辑前后自由插入额外操作而目标类本身完全不需要感知这些变化。比如权限拦截// 业务代码只关心保存用户 userService.saveUser(user); // 但实际执行时调用链可能是这样的 // 代理对象.checkPermission(user); // 代理对象.logOperation(saveUser); // 目标对象.saveUser(user);这样做的价值在于业务代码保持纯粹横切逻辑单独沉淀改动成本极低。后续想加缓存、加限流、加审计只需要在代理层做调整不需要惊动业务实现。1.2 生活化类比中介干的活和代理一模一样用房产中介来类比会更好理解。房东有一套房子要出租正常情况下租客直接找房东签合同就行。但问题是房东没时间应付所有看房者也不想暴露太多个人信息更不想挨个筛选租客。这时候房东把房源挂在中介那里中介代替房东接待客户、带看、谈价、走流程最后房东在签约环节露面就行。这个场景里房东就是目标对象中介就是代理对象租房需求就是被代理的方法调用。中介在带看过程中加了什么加了筛选、预约、讲解、合同审核这些额外的逻辑而房东实际的“出租行为”本身并没有改变。这就是代理模式在生活中的直接映射。那代理模式有哪些典型的应用领域我简单整理一下访问控制权限校验、黑白名单、接口限流代理类在执行方法前判断。日志审计方法入参、出参、耗时、异常信息的统一采集业务代码无感知。延迟加载大对象初始化很贵通过代理先返回一个轻量占位对象真正用到时才创建真实对象。远程调用本地调用代理代理负责网络传输、序列化目标对象在远程服务器上比如RPC框架。事务管理Spring事务的本质就是代理代理在方法前开启事务方法后提交或回滚。1.3 从静态代理到字节码增强一条完整的演进线代理模式的落地方式并不是一开始就到字节码增强的它经历了一个逐步动态化、底层化的过程实现方式核心机制生成时机复杂度典型应用静态代理手写代理类编译期确定代理关系编译期低学习入门、简单包装JDK动态代理反射 动态生成接口实现类运行时中Spring AOP、MyBatis MapperCGLIB动态生成目标类的子类运行时中高Spring AOP、Hibernate字节码增强直接修改/生成class字节码编译期或运行时高Lombok、Arthas、SkyWalking、Mockito看到这个表格你会发现从静态代理到字节码增强本质上是一条“控制力越来越强自由度越来越高”的路线。越往后走越接近Java运行机制的本质也越能解释框架底层的设计选择。2. 静态代理最直观的“手动挡”方案2.1 手写一个静态代理看懂代理最原始的样子静态代理是最容易理解的实现方式因为它没有任何花哨的运行时机制全靠代码一行行写出来。下面是标准的三步走第一步定义一个业务接口public interface UserService { void saveUser(User user); }第二步实现真实业务逻辑public class UserServiceImpl implements UserService { Override public void saveUser(User user) { System.out.println(保存用户 user.getName()); // 真正业务逻辑... } }第三步手工编写代理类同样实现UserService接口内部持有目标对象并在调用前后插入增强逻辑public class UserServiceStaticProxy implements UserService { private final UserService target; public UserServiceStaticProxy(UserService target) { this.target target; } Override public void saveUser(User user) { System.out.println([代理] 权限校验开始); System.out.println([代理] 记录日志准备保存用户 user.getName()); long start System.currentTimeMillis(); target.saveUser(user); long cost System.currentTimeMillis() - start; System.out.println([代理] 方法耗时 cost ms); System.out.println([代理] 记录日志保存完成); } }调用端的变化在于业务代码拿到的不是UserServiceImpl而是它的代理对象UserService service new UserServiceStaticProxy(new UserServiceImpl()); service.saveUser(user);这段代码把代理模式的核心逻辑展示得很清楚代理类和目标类实现同一个接口代理类持有目标对象引用调用时把逻辑转发给目标同时在外围加料。这就是静态代理的全部秘密没有更高深的东西了。2.2 静态代理的优势和它最大的痛点静态代理的优点很明显实现简单没有反射和动态生成的消耗性能几乎无损调试友好所有代码都在源码里打断点也好查。我现在带新人学设计模式还是会让他们先手写一遍静态代理因为这是理解代理概念的“最小可行案例”。但静态代理的缺点同样致命而且随着业务规模扩大这些缺点会越来越难受类爆炸。每要代理一个接口就得手写一个代理类。如果系统里有几十个接口就得写几十个代理类维护成本直线上升。代码重复。权限校验、日志记录这类逻辑在每个代理类里几乎一模一样复制粘贴导致大量重复代码。侵入性高。接口新增方法实现类和代理类都要同步改漏改一个就出bug。改造成本大。如果需要给所有代理类增加一个新逻辑比如加个Redis缓存你得把所有代理类全部打开挨个改这是极其痛苦的事情。我在实际项目里见到过类似的“伪静态代理地狱”一个老系统里有两个Service的日志逻辑分别写在各自代理类里后来要统一调整日志格式改动范围波及了十几个文件还漏改了一个导致线上日志格式不统一排查问题费了半天劲。这就是典型的静态代理扩展性差带来的真实代价。所以静态代理适合解决“单点、低频、需求稳定”的代理场景一旦代理目标多起来你必须寻求更动态的方案。2.3 静态代理和装饰器模式到底有什么区别这里顺便说一个容易被混淆的知识点。装饰器模式看起来和静态代理长得特别像都要包装一个目标对象都要在调用前后加逻辑。但两者的目的有本质区别代理模式强调“控制访问”——我限制你、检查你、替你做决定目标对象甚至可以是远程的。装饰器模式强调“增强功能”——比如给咖啡加奶、加糖目标对象必须在本地且真实存在装饰器一层层套上去目的是动态丰富能力。举个例子BufferedInputStream包装FileInputStream这是装饰器目的是增加缓冲能力而Spring的TransactionProxy包装业务Bean这是代理目的是控制事务边界。理解这个区别在面试里被问到“代理模式和装饰器模式的区别”时才能答出深度。3. JDK动态代理反射带来的“自动挡”体验3.1 核心机制Proxy类与InvocationHandler接口静态代理最大的痛点是“一个接口就要写一个代理类”那能不能只写一个通用处理器在运行时自动生成代理类JDK动态代理就是来解决这个问题的。JDK动态代理包含两个核心角色InvocationHandler一个接口里面只有一个invoke方法。你在这个方法里写增强逻辑它会在代理对象调用任意方法时被触发。ProxyJDK提供的一个工具类核心方法是newProxyInstance()负责在运行时动态创建代理类对象。先看代码还是用UserService的例子但这次不再需要手写代理类了只写一个通用的InvocationHandlerpublic 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 { System.out.println([动态代理] 调用前 method.getName()); long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println([动态代理] 调用后耗时 cost ms); return result; } }然后通过Proxy类一行代码生成代理对象UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class[]{UserService.class}, new LogInvocationHandler(target) ); proxy.saveUser(user);注意这个代理对象是运行时动态生成的代码里没有任何一个“UserServiceProxy”类。这就解决了类爆炸问题——不管你有多少接口只要实现一个InvocationHandler就能对所有接口生效。3.2 JDK动态代理的底层运行过程很多人会用JDK动态代理但不知道它底层到底做了什么。面试官也喜欢深挖这一层必须把原理讲清楚。当你调用Proxy.newProxyInstance()时底层会做这几件事根据传入的接口列表用ProxyGenerator生成一个代理类的字节码。这个代理类继承了Proxy类并实现了你传入的所有接口。代理类的构造方法接收一个InvocationHandler参数。代理类每个接口方法的实现逻辑都是一样的把方法调用转发给InvocationHandler.invoke()。所以整个调用链路实际上是业务代码 - 代理对象.method() - InvocationHandler.invoke() - 反射调用目标对象.method()再看代理类的大致形态伪代码public final class $Proxy0 extends Proxy implements UserService { private static Method m3; public $Proxy0(InvocationHandler h) { super(h); } Override public final void saveUser(User user) { // 把调用转发给InvocationHandler super.h.invoke(this, m3, new Object[]{user}); } }想把动态生成的代理类保存下来看细节可以设置一个系统参数-Dsun.misc.ProxyGenerator.saveGeneratedFilestrue然后在项目根目录的com/sun/proxy/下就能看到生成的$Proxy0.class文件用javap -c反编译一下代理类的内部结构一目了然。3.3 JDK动态代理的致命限制只能代理接口JDK动态代理用起来很爽但它有一个硬性约束——被代理对象必须至少实现一个接口因为动态生成的代理类继承的是Proxy类Java不支持多继承所以只能通过实现接口来保证代理类和目标类的方法签名一致。这意味着如果你有一个没有实现任何接口的普通类比如public class OrderService { public void createOrder(String orderId) { // 业务逻辑 } }直接用JDK动态代理是行不通的Proxy.newProxyInstance()第二个参数传不了OrderService.class因为它不是接口。那怎么办两种思路给类强行加接口改代码结构。换一种代理技术不通过接口直接通过继承来生成代理子类这就是CGLIB做的事。我在做技术选型时给团队定的原则很简单如果类本身有接口优先用JDK动态代理因为它是JDK原生能力不依赖第三方包如果没有接口再考虑CGLIB或方案调整。4. CGLIB代理突破接口限制的运行时子类化4.1 原理通过继承生成子类重写非final方法CGLIBCode Generation Library的实现思路和JDK动态代理完全不同。它直接在运行时生成目标类的一个子类子类重写目标类的非final方法然后在重写的方法里插入增强逻辑。因为用的是继承所以CGLIB不需要目标类实现任何接口。但这里引出一个关键问题JDK动态代理用反射调用目标方法性能有额外损耗CGLIB通过子类重写调用增强方法时不需要反射吗也不是。CGLIB底层用了一个叫FastClass的机制它生成两个额外的类一个是FastClass索引类一个是目标对象的FastClass类通过方法索引直接调用避免了反射的频繁方法查找。所以CGLIB在调用性能上通常优于JDK动态代理但对象创建时的字节码生成成本更高。4.2 用Enhancer实现CGLIB代理CGLIB不像JDK动态代理那样属于JDK标准库需要额外引入依赖。如果用Maven这样添加dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency核心API是Enhancer和MethodInterceptor。下面是一个标准用法public class CglibProxyFactory { public static Object createProxy(Class? targetClass) { Enhancer enhancer new Enhancer(); // 设置被代理的目标类CGLIB会生成它的子类 enhancer.setSuperclass(targetClass); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) - { System.out.println([CGLIB] 调用前 method.getName()); Object result proxy.invokeSuper(obj, args); System.out.println([CGLIB] 调用后); return result; }); return enhancer.create(); } }注意这里MethodInterceptor是net.sf.cglib.proxy.MethodInterceptor不是Spring AOP里的org.aopalliance.intercept.MethodInterceptor两个包名虽然方法签名类似但来源不同写代码的时候别引用错了。调用方式OrderService proxy (OrderService) CglibProxyFactory.createProxy(OrderService.class); proxy.createOrder(20250101);OrderService这个类没有实现任何接口但CGLIB照样可以代理这就补上了JDK动态代理的短板。4.3 CGLIB的局限性final方法、final类、私有方法CGLIB虽然不要求接口但继承机制本身给它带来了一些限制final类无法代理。final类不能被继承子类都生成不了更别说代理了。final方法无法增强。子类可以继承final方法但不能重写所以增强逻辑不会生效。private方法不会被代理。子类根本看不到父类的private方法所以无法拦截。static方法同样无法增强。还有一个容易踩的坑是类内部方法相互调用时代理不生效。比如目标类里有public方法a()a()内部调用了this.b()即使你通过代理对象调用a()b()里的增强逻辑也不会执行。因为代理对象和原始对象是两个不同的对象this指向的是原始对象不是代理对象。我在一个实时项目中遇到过这个问题给某个Service的耗时统计做了CGLIB代理结果发现统计出来的时间不对部分内部方法的耗时没被记录。排查下来就是这个原因内部调用直接走this绕过了代理。解决方案也很粗暴把需要增强的方法拆到另一个Bean里注入进来通过Bean引用调用走的就是代理了。4.4 Spring AOP到底用哪个取决于你的配置Spring AOP的代理原理就是前面提到的两种动态代理结合。Spring Framework早期的默认策略是如果目标Bean实现了接口用JDK动态代理如果没有实现接口自动切换CGLIB。这样做是为了尽量减少第三方依赖因为CGLIB一开始不是Spring必须的依赖。但Spring Boot 2.x开始情况发生了变化。Boot 2.x默认把spring.aop.proxy-target-classtrue设为了默认值也就是说优先使用CGLIB代理。原因主要是JDK动态代理强制要求实现接口这件事在很多场景下给使用者造成了困扰比如返回类型是具体类而不是接口时强转会报ClassCastException。你可以在application.yml里手动调整spring: aop: proxy-target-class: true # trueCGLIBfalseJDK动态代理需要说明的是这个配置只对Spring AOP的切面代理生效对于一些必须使用JDK动态代理的场景比如Spring对接口类型的某些内部处理依然会走JDK动态代理。5. 字节码增强直接操作class文件的终极手段5.1 从“生成代理类”到“改写任意字节码”聊到这里你可能发现一个事实JDK动态代理和CGLIB底层其实也在生成字节码只是它们把这件事封装好了你感知不到而已。那更进一步如果需求不是“给某个对象加代理”而是“给某个类的方法直接植入一行代码”呢比如线上排查问题希望在不重新发布的情况下往某个类的某个方法里临时加入口日志。或者做一个APM工具统计所有Controller方法的耗时但又不希望对业务代码做任何侵入。这些场景远远超出了动态代理的范畴你需要的是字节码增强。字节码增强的核心思路是Java源码被编译成.class文件后里面是JVM能识别的字节码指令。在类加载之前甚至运行时直接读取、修改、生成字节码实现对类的深度改造。这层能力是做框架、做中间件、做工具时必须掌握的技能。实现字节码增强的常见技术栈技术栈特点典型代表Javassist基于源码级别的API可以写Java代码片段插入方法上手快Hibernate、MyBatis早期ASM直接操作字节码指令性能极高但API粒度细上手门槛高Spring、Groovy、Arthas、SkyWalkingByteBuddy封装ASM提供更友好的API生成高性能字节码兼容性好Mockito、Hibernate、IllegalStateException检测Java Instrumentation官方提供的运行时修改字节码的机制结合Agent使用Arthas、JProfiler、各种APM工具5.2 用Javassist快速体验一把方法改写Javassist是理解字节码增强的最佳入门工具因为它的API是“源码头”的你可以在不需要懂字节码指令的情况下用写普通Java代码的方式修改类的行为。引入依赖dependency groupIdorg.javassist/groupId artifactIdjavassist/artifactId version3.29.2-GA/version /dependency假设我有一个简单的业务类public class PaymentService { public void pay(String account, BigDecimal amount) { System.out.println(执行支付 account 金额 amount); } }现在我要在不改源码的情况下给pay方法增加耗时统计。用Javassist这样做public class JavassistDemo { public static void main(String[] args) throws Exception { ClassPool pool ClassPool.getDefault(); CtClass cc pool.get(com.example.demo.PaymentService); CtMethod method cc.getDeclaredMethod(pay); // 在方法开头插入代码 method.insertBefore(long start System.currentTimeMillis();); // 在方法结尾插入代码finally块中输出耗时 method.insertAfter(System.out.println(\[Javassist] 耗时\ (System.currentTimeMillis() - start) \ms\);); // 加载修改后的类 Class? clazz cc.toClass(); PaymentService service (PaymentService) clazz.getDeclaredConstructor().newInstance(); service.pay(alice, new BigDecimal(100.00)); } }运行结果会在执行pay方法的同时自动输出耗时信息。这就是字节码增强最直观的体验——类的行为被改了但业务源码一个字没动。Javassist内部会把你写的Java代码片段“翻译”成字节码指令插入到目标方法的字节码序列里。Javassist的适用场景通常是工具类、压测mock、热部署以及一些对性能要求不是很极端的框架场景。它的优点是开发效率高逻辑清晰缺点是相比ASM生成的字节码性能略差且Javassist内置的编译器版本落后于JDK新语法比如对lambda的支持有限处理复杂场景时会遇到限制。5.3 ASM与Java Agent做一个真正的字节码“手术师”如果你需要处理高性能场景或者需要精细控制字节码的每一个指令那就得用ASM。ASM的设计核心是Visitor模式。它提供一个ClassReader负责把class文件读成事件流一个ClassWriter负责把访问结果重新写成class文件中间挂上各种Visitor在访问到类结构、方法、指令时做修改。看一个最简单的ASM改动方法逻辑的例子——这里只展示结构骨架ClassReader cr new ClassReader(originalClassBytes); ClassWriter cw new ClassWriter(ClassWriter.COMPUTE_FRAMES); ClassVisitor cv new ClassVisitor(Opcodes.ASM9, cw) { Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv super.visitMethod(access, name, descriptor, signature, exceptions); if (pay.equals(name)) { return new MethodVisitor(Opcodes.ASM9, mv) { Override public void visitCode() { super.visitCode(); // 在方法开头植入 System.out.println(pay called); mv.visitFieldInsn(Opcodes.GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;); mv.visitLdcInsn(pay called); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/io/PrintStream, println, (Ljava/lang/String;)V, false); } }; } return mv; } }; cr.accept(cv, ClassReader.SKIP_DEBUG); byte[] enhancedClass cw.toByteArray();这里有很多硬核的字节码概念GETSTATIC、LDC、INVOKEVIRTUAL每一个都是JVM指令。正因为如此ASM的学习曲线相当陡峭。但高性能框架喜欢它因为ASM生成的字节码可以做到和手写代码几乎一样快而且ASM不加载目标类不会触发类初始化非常轻量。另一个重要组件是Java Agent。它是JVM官方提供的一种跨进程字节码增强机制。你在JVM启动参数里加上-javaagent:xxx.jarJVM在加载类的时候就会先经过Agent的逻辑你可以在premain方法里通过Instrumentation对类进行字节码转换。一个最简单的Agent结构是这样的public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) - { if (com/example/demo/PaymentService.equals(className)) { // 这里用ASM或Javassist修改classfileBuffer返回新的字节码 return modifiedClassBytes; } return null; }); } }Arthas、SkyWalking这些工具本质就是Java Agent加上字节码增强在类加载的时候动态植入埋点逻辑。理解了这一层你再看这些工具就不会觉得它们神秘了。5.4 框架里的字节码增强你以为的魔法其实都是套路了解完原理我们再回看主流框架会发现字节码增强其实无处不在Lombok编译期通过注解处理器JSR 269修改抽象语法树在生成的class里加入getter/setter/构造器等方法。严格说这不是运行时字节码修改但核心思想一致。MyBatisMapper接口通过JDK动态代理生成实现类每个Mapper方法被代理到MapperProxy上执行SQL逻辑。Hibernate实体类懒加载时通过字节码增强生成代理子类。Mockito新版本默认使用ByteBuddy生成Mock类ByteBuddy底层是ASM。Arthas通过Java Agent在运行期动态增强目标方法实现热更新、watch、trace等功能。SkyWalking同样通过Java Agent拦截插件里声明的类自动注入链路追踪代码。从动态代理到字节码增强本质上是一步步“逼近底层”的过程。JDK动态代理和CGLIB把常见代理想法封装成了高级API而字节码增强是“这个封装库底层的通用能力”。如果哪天你想写一个自研的APM、自研的ORM、自研的RPC框架你就绕不开字节码增强。6. 从面试到实战高频考点与排坑指南6.1 面试必问的几个问题怎么回答才有深度我把代理模式相关的面试问题整理一下这些问题看起来基础但能答出深度的人确实不多。问题一JDK动态代理和CGLIB有什么区别基础回答JDK动态代理只能代理接口CGLIB通过继承可以代理类JDK动态代理用反射CGLIB用FastClassSpring AOP默认根据类是否实现接口选择代理方式。深度加分项JDK动态代理生成的代理类实现了接口所以调用时是一种接口引用 - 代理类的多态调用CGLIB是子类重写父类方法所以final方法无法被增强CGLIB在创建代理对象时需要生成字节码首次创建性能不如JDK动态代理但方法调用时因为避免反射长期运行的综合性能更好。问题二为什么Spring AOP不用静态代理回答思路静态代理每代理一个方法或类都要单独写代理类扩展性太差。Spring AOP需要管理大量Bean的横切逻辑动态代理可以在运行时统一生成代理对象且逻辑集中在一个InvocationHandler或MethodInterceptor里灵活性和维护性都远优于静态代理。问题三代理对象和目标对象是什么关系深入一点说JDK动态代理的代理对象和目标对象都实现了同一个接口但两者没有继承关系CGLIB代理对象是目标对象的子类。所以做类型判断时要小心代理对象用instanceof去判断接口类型是ok的但如果是具体类类型JDK动态代理会直接ClassCastException除非用CGLIB。问题四动态代理一定比静态代理慢吗不一定。静态代理因为不经过反射最简单的调用上确实快但真实业务场景里方法调用本身可能耗时已经很高代理反射带来的额外损耗在绝大多数场景下可以忽略。而且CGLIB的FastClass机制在调用性能上已经很接近原生调用。不要盲目为了性能用静态代理工程上维护成本才是主要矛盾。6.2 实战中的坑代理对象引发的诡异问题在真实项目里代理引发的编译期不报错、运行期才炸的问题特别多这里列几个我踩过和看别人踩过的。ClassCastException坚称自己是UserService却转不了具体类UserService service ...; // 实际是JDK动态代理对象 UserServiceImpl impl (UserServiceImpl) service; // 运行期报错因为$Proxy0虽然实现了UserService接口但它并不是UserServiceImpl的子类强转必炸。解决办法就是别用具体类接收代理对象或者Spring Boot里设置proxy-target-classtrue强制CGLIB。方法内部this调用导致代理增强不生效CGLIB代理下public方法内部的this.method()调用不会走代理。这是面试官最爱问的“为什么Spring事务加了注解却不生效”的底层原因之一。解决方案是自注入代理对象通过Lazy注入自身或者拆类。InvocationHandler里死循环调用新手写JDK动态代理时invoke方法内部最容易写错Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 错误写法再次调用proxy方法会重新进入invoke死循环 Object result proxy.getClass().getMethod(method.getName()).invoke(proxy, args); // 正确写法调用目标对象 Object result method.invoke(target, args); return result; }这个坑很多入门者都踩过stackoverflow上经典问题之一。代理对象序列化问题代理类在序列化时可能会因为内部类、类加载器问题报NotSerializableException。如果业务中需要缓存、RPC传递代理对象尽量传dto别传代理对象。6.3 一个综合案例无侵入给Mapper接口加日志增强最后写一个综合小案例把JDK动态代理用在实际工作场景里增加一点工程感。假设系统里有很多Mapper接口每个Mapper方法执行时你都想打印SQL执行耗时和入参方便排查问题。最通用的做法是写一个基于JDK动态代理的通用日志增强Handlerpublic class MapperLogHandler implements InvocationHandler { private final Object target; public MapperLogHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); try { Object result method.invoke(target, args); System.out.println([SQL-LOG] method.getDeclaringClass().getSimpleName() . method.getName() 耗时 (System.currentTimeMillis() - start) ms); return result; } catch (Throwable t) { System.out.println([SQL-LOG] method.getDeclaringClass().getSimpleName() . method.getName() 异常 t.getMessage()); throw t; } } }然后写一个工厂方法传入Mapper接口类型和真实实现返回代理对象public static T T createMapperProxy(ClassT mapperInterface, T target) { return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[]{mapperInterface}, new MapperLogHandler(target) ); }Mapper接口天然是接口JDK动态代理正好派上用场。你用的时候只需要把原来的Mapper实例包一层业务侧完全无感知。这个思路和MyBatis内部MapperProxy的机制是相通的理解了它再看MyBatis源码会觉得特别亲切。字节码增强部分也一样如果项目中想接一个统一埋点组件你可以选择在编译期用Lombok式注解处理也可以选择在运行时用Java Agent统一植入。不同阶段的项目、不同合规需求选型会不同但底层的原理是共通的只要能把字节码改对Java世界里就没有那么多“黑魔法”。我以前刚学Spring AOP那会儿只知道加个Transactional注解就有事务了后来翻到cglib的代理类反编译代码才真正理解“代理”这两个字的分量。如果大家顺着静态代理手写一遍再跑一遍JDK动态代理再用Javassist改一个方法这一串走下来你会发现所有框架的“魔法”都回归到了这些最朴素的技术点上。后续如果再遇到代理不生效、类型转换异常、注解丢失之类的问题排查的思路也会清晰很多。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。