代理模式全解析:从静态代理到JDK动态代理、CGLIB与字节码增强
2026/9/24 21:18:01 网站建设 项目流程

只要有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 Mapper
CGLIB动态生成目标类的子类运行时中高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方法。你在这个方法里写增强逻辑,它会在代理对象调用任意方法时被触发。
  • Proxy:JDK提供的一个工具类,核心方法是newProxyInstance(),负责在运行时动态创建代理类对象。

先看代码,还是用UserService的例子,但这次不再需要手写代理类了,只写一个通用的InvocationHandler:

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 { 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()时,底层会做这几件事:

  1. 根据传入的接口列表,用ProxyGenerator生成一个代理类的字节码。
  2. 这个代理类继承了Proxy类,并实现了你传入的所有接口。
  3. 代理类的构造方法接收一个InvocationHandler参数。
  4. 代理类每个接口方法的实现逻辑都是一样的:把方法调用转发给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.saveGeneratedFiles=true

然后在项目根目录的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方法

CGLIB(Code Generation Library)的实现思路和JDK动态代理完全不同。它直接在运行时生成目标类的一个子类,子类重写目标类的非final方法,然后在重写的方法里插入增强逻辑。因为用的是继承,所以CGLIB不需要目标类实现任何接口。

但这里引出一个关键问题:JDK动态代理用反射调用目标方法,性能有额外损耗;CGLIB通过子类重写,调用增强方法时不需要反射吗?也不是。CGLIB底层用了一个叫FastClass的机制,它生成两个额外的类,一个是FastClass索引类,一个是目标对象的FastClass类,通过方法索引直接调用,避免了反射的频繁方法查找。所以CGLIB在调用性能上通常优于JDK动态代理,但对象创建时的字节码生成成本更高。

4.2 用Enhancer实现CGLIB代理

CGLIB不像JDK动态代理那样属于JDK标准库,需要额外引入依赖。如果用Maven,这样添加:

<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>

核心API是EnhancerMethodInterceptor。下面是一个标准用法:

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(); } }

注意这里MethodInterceptornet.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-class=true设为了默认值,也就是说优先使用CGLIB代理。原因主要是JDK动态代理强制要求实现接口这件事,在很多场景下给使用者造成了困扰,比如返回类型是具体类而不是接口时,强转会报ClassCastException。你可以在application.yml里手动调整:

spring: aop: proxy-target-class: true # true=CGLIB,false=JDK动态代理

需要说明的是,这个配置只对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、SkyWalking
ByteBuddy封装ASM,提供更友好的API,生成高性能字节码,兼容性好Mockito、Hibernate、IllegalStateException检测
Java Instrumentation官方提供的运行时修改字节码的机制,结合Agent使用Arthas、JProfiler、各种APM工具

5.2 用Javassist快速体验一把方法改写

Javassist是理解字节码增强的最佳入门工具,因为它的API是“源码头”的,你可以在不需要懂字节码指令的情况下,用写普通Java代码的方式修改类的行为。

引入依赖:

<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.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();

这里有很多硬核的字节码概念:GETSTATICLDCINVOKEVIRTUAL,每一个都是JVM指令。正因为如此,ASM的学习曲线相当陡峭。但高性能框架喜欢它,因为ASM生成的字节码可以做到和手写代码几乎一样快,而且ASM不加载目标类,不会触发类初始化,非常轻量。

另一个重要组件是Java Agent。它是JVM官方提供的一种跨进程字节码增强机制。你在JVM启动参数里加上-javaagent:xxx.jar,JVM在加载类的时候就会先经过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/构造器等方法。严格说这不是运行时字节码修改,但核心思想一致。
  • MyBatis:Mapper接口通过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用FastClass;Spring 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-class=true强制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动态代理的通用日志增强Handler:

public 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(Class<T> 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改一个方法,这一串走下来,你会发现所有框架的“魔法”都回归到了这些最朴素的技术点上。后续如果再遇到代理不生效、类型转换异常、注解丢失之类的问题,排查的思路也会清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询