Java 动态代理(Dynamic Proxy) 是 Java 反射机制的核心应用,也是 Spring AOP(面向切面编程,Aspect-Oriented Programming)、MyBatis、RPC 框架的底层基石,属于 Java 进阶必学的高频知识点。然而很多初学者陷入“只会用框架、不懂底层原理”的困境——知道 AOP 能加日志,却说不出 JDK 动态代理和 CGLIB(Code Generation Library,代码生成库)的根本区别,面试被问到“为什么 JDK 动态代理只能代理接口”时直接卡壳。本文将从痛点出发,先对比静态代理的局限,再深入解析 JDK 动态代理与 CGLIB 的底层原理、关系与选型,搭配可运行的代码示例和面试高频题,帮你建立完整知识链路。
一、痛点切入:为什么需要动态代理?

先来看一个最常见的问题:在不修改原有代码的前提下,为业务方法统一添加日志记录。
假设有一个业务接口和实现类:

// 业务接口 public interface UserService { void addUser(String username); void deleteUser(int id); } // 实现类 public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("添加用户:" + username); } @Override public void deleteUser(int id) { System.out.println("删除用户:" + id); } }
静态代理的做法:为 UserService 手动编写一个代理类,实现相同的接口,在每个方法调用前后加入日志逻辑。
public class UserServiceStaticProxy implements UserService { private UserService target; public UserServiceStaticProxy(UserService target) { this.target = target; } @Override public void addUser(String username) { System.out.println("[LOG] 开始执行 addUser"); target.addUser(username); System.out.println("[LOG] 执行完成"); } @Override public void deleteUser(int id) { System.out.println("[LOG] 开始执行 deleteUser"); target.deleteUser(id); System.out.println("[LOG] 执行完成"); } }
静态代理的致命缺点:
代码冗余:接口每增加一个方法,代理类就得多写一个方法。
维护成本高:项目中如果有几十个接口需要增强,就要写几十个代理类。
复用性差:日志、事务等通用逻辑无法被多个目标类复用。
动态代理正是为了解决这些问题而生的:在运行时动态生成代理类,统一处理方法调用,实现“无侵入式代码扩展” -。
二、核心概念讲解:JDK 动态代理
2.1 标准定义
JDK 动态代理(JDK Dynamic Proxy) 是 Java 标准库(java.lang.reflect 包)提供的动态代理机制。它要求目标对象必须实现至少一个接口,通过反射在运行时生成实现相同接口的代理类,将所有方法调用转发到 InvocationHandler 进行处理-1-3。
2.2 生活化类比
把 JDK 动态代理想象成 “总机电话转接系统” :你只需要拨打总机号码(调用代理对象),总机会根据你按的号码(方法名),自动转接到对应部门(invoke() 方法中处理),然后在接通前后可以加上录音提醒(增强逻辑)。你永远不需要直接拨打各分机号码,也不需要知道分机内部是怎么工作的。
2.3 核心三要素
| 组件 | 作用 |
|---|---|
InvocationHandler 接口 | 定义代理逻辑,你需要实现它的 invoke() 方法,在其中编写“方法调用前/后”的增强代码 |
Method 类 | 反射中代表一个具体的方法对象,通过 method.invoke(target, args) 可以动态调用目标方法 |
Proxy 类 | JDK 提供的工具类,核心方法 newProxyInstance() 用于动态创建代理对象 |
💡 注意:调用 Proxy.newProxyInstance() 时传入的是目标对象的接口数组,而不是目标类的 Class 对象,这正是“必须基于接口”的根源所在。
2.4 工作原理
// 客户端调用代理对象的方法 proxy.addUser("张三"); ↓ // JVM 将调用转发给 InvocationHandler 的 invoke() 方法 handler.invoke(proxy, method, args); ↓ // invoke() 中执行前置增强逻辑 System.out.println("[LOG] 开始执行"); ↓ // 通过反射调用目标对象的真实方法 method.invoke(target, args); ↓ // invoke() 中执行后置增强逻辑 System.out.println("[LOG] 执行完成"); ↓ // 返回结果给客户端
JDK 动态代理会在运行时为接口生成一个代理类(通常命名为 $Proxy0),当调用代理对象的方法时,本质是反射调用目标方法-1。
三、关联概念讲解:CGLIB 动态代理
3.1 标准定义
CGLIB(Code Generation Library) 是一个基于 ASM 字节码生成框架的第三方库。它不需要目标类实现接口,而是通过继承目标类生成子类的方式实现代理,重写父类的非 final 方法,并将调用拦截到 MethodInterceptor.intercept() 方法中-。
3.2 工作原理
CGLIB 的核心类是 Enhancer 和 MethodInterceptor:
// 创建 Enhancer 实例 Enhancer enhancer = new Enhancer(); // 设置父类(即目标类) enhancer.setSuperclass(TargetClass.class); // 设置回调(即 MethodInterceptor 实现) enhancer.setCallback(methodInterceptor); // 生成代理对象(目标类的子类实例) TargetClass proxy = (TargetClass) enhancer.create();
当调用代理对象的方法时,调用会被 MethodInterceptor.intercept() 拦截,在其中可以编写增强逻辑,再通过 MethodProxy.invokeSuper() 调用父类(即原目标类)的方法-。
⚠️ 限制:由于 CGLIB 采用继承机制,final 类无法被代理,final 方法也无法被重写和增强-1。
3.3 为什么需要 CGLIB?
很多业务类(尤其是遗留代码)并没有实现接口,比如:
public class EmailService { public void sendEmail(String to, String content) { System.out.println("发送邮件至:" + to); } }
JDK 动态代理无法代理 EmailService,因为它是普通类而非接口。CGLIB 通过生成子类的方式解决了这个问题,因此在实际框架中,CGLIB 是对 JDK 动态代理的重要补充。
四、概念关系与区别总结
| 对比维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 代理方式 | 基于接口 | 基于继承(生成子类) |
| 必要条件 | 目标类必须实现至少一个接口 | 目标类不能是 final,方法不能是 final |
| 底层技术 | 反射 + Proxy(Java 原生) | ASM 字节码增强(第三方库) |
| 依赖 | 无需第三方库,JDK 内置 | 需要引入 CGLIB 库(Spring Core 已内置) |
| 性能特点 | 代理对象创建快,方法调用依赖反射 | 代理对象创建较慢,但方法调用接近直接调用 |
| 版本趋势 | JDK 8+ 反射优化后性能差距缩小 | 字节码技术稳定,适用高频调用 |
| 限制 | 只能代理接口中声明的方法 | 无法代理 final 类和 final 方法 |
一句话概括两者关系:JDK 动态代理是“只能给有身份证(接口)的人办事”,CGLIB 是“谁都能代理,但别给自己上锁(final)” —— 一个是官方原生方案,一个是功能更强的第三方补充。
关于性能,不同 JDK 版本表现差异显著:JDK 7 在小调用量(约 100 万次)时 JDK 动态代理比 CGLIB 快约 30%,JDK 8 及以后性能差距已大幅缩小-1。在 JDK 17+ 环境中,两者在绝大多数业务场景下的性能差异可以忽略不计,建议优先从架构清晰度和代码可维护性出发做选择。
五、代码示例演示
5.1 JDK 动态代理完整示例
// 1. 定义接口 public interface Calculator { int add(int a, int b); } // 2. 实现类 public class CalculatorImpl implements Calculator { @Override public int add(int a, int b) { return a + b; } } // 3. 实现 InvocationHandler,编写增强逻辑 import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; 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("[JDK] 调用方法:" + method.getName() + ",参数:" + args[0] + ", " + args[1]); long start = System.currentTimeMillis(); Object result = method.invoke(target, args); // 反射调用目标方法 long end = System.currentTimeMillis(); // 后置增强:性能监控 System.out.println("[JDK] 方法返回值:" + result + ",耗时:" + (end - start) + "ms"); return result; } } // 4. 创建代理对象并调用 public class JdkProxyDemo { public static void main(String[] args) { Calculator target = new CalculatorImpl(); Calculator proxy = (Calculator) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 需要代理的接口数组 new LogInvocationHandler(target) // InvocationHandler 实例 ); int result = proxy.add(3, 5); System.out.println("最终结果:" + result); } }
输出结果:
[JDK] 调用方法:add,参数:3, 5 [JDK] 方法返回值:8,耗时:0ms 最终结果:8
5.2 CGLIB 动态代理示例
// 引入 CGLIB 依赖(Maven) // <dependency> // <groupId>cglib</groupId> // <artifactId>cglib</artifactId> // <version>3.3.0</version> // </dependency> import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; // 1. 目标类(无需实现接口) public class EmailService { public void send(String to, String content) { System.out.println("发送邮件至:" + to + ",内容:" + content); } } // 2. 实现 MethodInterceptor public class LogMethodInterceptor 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; } } // 3. 创建代理对象 public class CglibProxyDemo { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(EmailService.class); // 设置父类 enhancer.setCallback(new LogMethodInterceptor()); // 设置回调 EmailService proxy = (EmailService) enhancer.create(); proxy.send("test@example.com", "Hello CGLIB!"); } }
5.3 新旧实现方式对比
| 对比项 | 静态代理 | JDK 动态代理 |
|---|---|---|
| 代理类数量 | 每个接口写一个代理类 | 一个 InvocationHandler 可复用 |
| 接口新增方法 | 代理类必须同步新增方法 | 无需修改,自动代理 |
| 代码侵入 | 高 | 零侵入 |
| 运行时灵活性 | 编译期确定 | 运行时动态确定 |
直观效果:静态代理接口有 100 个方法,代理类必须实现 100 个方法;JDK 动态代理无论接口有多少方法,InvocationHandler 中只需要写一个 invoke() 方法即可统一处理。
六、底层原理与技术支撑
6.1 技术栈全景
| 层级 | 技术 | 说明 |
|---|---|---|
| 底层支撑 | Java 反射(Reflection) | 运行时获取类信息、动态调用方法,是 JDK 动态代理的基础 |
| JDK 方案 | Proxy + InvocationHandler | 生成接口的代理类,通过反射完成方法调用转发 |
| CGLIB 方案 | ASM 字节码框架 | 直接操作 Java 字节码,在运行时动态生成目标类的子类 |
| 框架封装 | Spring AOP | 根据目标类特征,在两者之间自动选择,并封装为声明式切面 |
6.2 各层关键机制
Java 反射 是动态代理的基石:通过 Class.forName() 获取类信息,Method.invoke() 动态执行方法,为代理层提供了“在运行时操作类和方法”的能力。
JDK 动态代理 在反射之上,通过 Proxy.newProxyInstance() 在运行时生成代理类的字节码,该代理类实现了指定的接口,并在每个接口方法内部调用 InvocationHandler.invoke()-17。
CGLIB 更进一步,不依赖反射调用目标方法,而是通过 ASM 直接生成字节码,生成的代理类是目标类的子类,方法调用直接通过子类继承实现,避免了反射开销-。
6.3 JDK 21+ 新变化
从 JDK 21 开始,JEP 451 对动态加载代理做出了调整:动态加载代理到正在运行的 JVM 时会发出警告,未来的 JDK 版本将默认禁止此行为-。这意味着:
启动时加载(通过
-javaagent参数)的代理不受影响。运行时动态 attach 代理的方式将逐步受限,需要关注后续版本变化。
对于绝大多数使用 Spring AOP、MyBatis 等框架的开发者来说,这一变化影响较小,因为这些框架的代理机制在应用启动阶段完成,不涉及运行时动态 attach。
七、高频面试题与参考答案
Q1:JDK 动态代理和 CGLIB 的区别有哪些?
参考答案(踩分点:代理方式 + 必要条件 + 底层技术 + 限制 + 应用场景):
代理方式不同:JDK 基于接口代理,CGLIB 基于继承生成子类代理。
必要条件不同:JDK 要求目标类必须实现接口;CGLIB 要求目标类不能是
final,目标方法不能是final。底层技术不同:JDK 使用 Java 反射 +
Proxy;CGLIB 使用 ASM 字节码框架。限制不同:JDK 只能代理接口中声明的方法;CGLIB 无法代理
final类和final方法。应用场景:Spring AOP 默认对有接口的类使用 JDK 代理,对无接口的类使用 CGLIB。
Q2:为什么 JDK 动态代理只能代理有接口的类?
参考答案(踩分点:代理类的生成方式 + 类型限制):
JDK 动态代理通过 Proxy.newProxyInstance() 生成代理对象时,需要传入目标类实现的接口数组,代理类本身会实现这些接口。Java 是单继承语言,生成的代理类已经继承了 Proxy 类,无法再继承目标类。JDK 动态代理只能通过“实现接口”的方式与目标类建立联系,从而要求目标类必须有接口-38。
Q3:Spring AOP 如何选择 JDK 动态代理还是 CGLIB?
参考答案(踩分点:自动判断逻辑 + 手动配置):
Spring AOP 通过 DefaultAopProxyFactory 自动判断:
如果目标类实现了接口且未强制使用 CGLIB,默认使用 JDK 动态代理;
如果目标类没有实现任何接口,或配置了
proxyTargetClass=true,则使用 CGLIB。
在 Spring Boot 2.x 及之后,当未指定 proxyTargetClass 时,框架会根据目标类是否有接口自动选择-。
Q4:静态代理和动态代理的核心区别是什么?
参考答案(踩分点:代码生成时机 + 维护成本 + 灵活性):
代码生成时机:静态代理在编译期手动编写代理类;动态代理在运行期由 JVM 动态生成代理类。
维护成本:静态代理的接口每增加一个方法,代理类必须同步修改;动态代理一个
InvocationHandler即可处理所有方法。灵活性:静态代理针对特定目标类,复用性差;动态代理可在运行时任意切换目标对象,通用性强。
Q5:final 方法能被代理吗?为什么?
参考答案(踩分点:JDK 和 CGLIB 分别说明):
JDK 动态代理:
final方法如果定义在接口中,可以被代理(接口方法不能是final);如果final方法是实现类自己的私有方法,不在接口中声明,则无法被 JDK 代理。CGLIB:无法代理
final方法,因为 CGLIB 通过生成子类并重写父类方法实现代理,而final方法不能被重写。
💡 加分点:JDK 动态代理代理的是接口方法,不关心实现类是否为 final;CGLIB 代理的是整个类,所以受 final 约束。
八、结尾总结
核心知识点回顾
| 序号 | 知识点 | 关键要点 |
|---|---|---|
| 1 | 动态代理的价值 | 运行时生成代理类,无侵入式代码增强,解决静态代理的代码冗余和维护困难问题 |
| 2 | JDK 动态代理 | 基于接口 + 反射 + Proxy/InvocationHandler,JDK 原生,轻量 |
| 3 | CGLIB | 基于继承 + ASM 字节码,无需接口,功能更强,但无法代理 final |
| 4 | 关系定位 | JDK 动态代理是官方方案,CGLIB 是接口缺失场景的补充,两者共同构成动态代理技术栈 |
| 5 | 框架应用 | Spring AOP 智能选择:有接口用 JDK,无接口用 CGLIB |
| 6 | 面试重点 | 两者区别 + JDK 只能代理接口的原因 + final 的限制 |
易错点提醒
⚠️ 常见误区一:认为 CGLIB 一定比 JDK 动态代理快。事实是 JDK 8+ 反射持续优化后,两者性能差距已大幅缩小,现代 JVM 中对普通业务场景的影响可以忽略-1。
⚠️ 常见误区二:认为 CGLIB 可以代理任何类。final 类无法被代理,final 方法无法被增强,这是 CGLIB 继承机制带来的根本限制。
⚠️ 常见误区三:混淆“动态代理”和“代理模式”。动态代理是实现,代理模式是设计思想 —— 动态代理是用反射和字节码技术在运行时实现代理模式的一种具体手段。
进阶方向预告
Spring AOP 源码剖析:从
@EnableAspectJAutoProxy到JdkDynamicAopProxy与CglibAopProxy的实现细节ByteBuddy 新一代字节码技术:比 CGLIB 更现代、更灵活的字节码生成方案
ASM 框架底层:直接操作 Java 字节码,实现最底层的动态代理
代理模式与装饰器模式的区别:两类结构型设计模式的核心差异与选型策略