代理模式是Java开发中使用频率最高、面试考察最密集的设计模式之一。无论是Spring AOP的声明式事务,还是RPC框架的远程调用封装,底层都离不开代理模式的身影。
很多学习者都有这样的体验:能在项目中用@Transactional注解,却说不清事务是怎么“织入”到方法中的;知道AOP能解耦日志、监控等横切逻辑,但被问到JDK和CGLIB的区别时支支吾吾;甚至面试时被追问“动态代理的代理对象为什么不能被强转为具体实现类”时当场卡壳。

本文借助贝壳AI助手的系统性资料整理能力,为你梳理Java代理模式从静态代理到动态代理(JDK + CGLIB)的完整知识链路。全文包含:痛点分析 → 核心概念讲解 → 代码示例 → 底层原理 → 面试考点,力求看得懂、记得住、用得上。
一、痛点切入:为什么需要代理模式?

先说一个真实场景。假设我们有一个用户服务接口,包含新增和删除两个方法:
public interface UserService { void addUser(String username); void deleteUser(String username); } public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("数据库新增用户:" + username); } @Override public void deleteUser(String username) { System.out.println("数据库删除用户:" + username); } }
某天,产品经理提出新需求:所有业务方法执行前要打印日志。最直接的写法就是在每个方法里手动加System.out.println,但10个、50个接口之后,代码中80%都是日志、监控、权限校验,核心业务逻辑反而被淹没-1。这就是典型的横切关注点(Cross-cutting Concerns) 侵入业务代码的问题。
传统“暴力”解决方案:静态代理
静态代理是代理模式最基础的实现方式,代理类在编译期就已确定,与目标类一一对应-5。实现步骤如下:
步骤1:定义业务接口(代理类和目标类的共同契约)
步骤2:编写目标类(真正执行业务逻辑)
步骤3:编写代理类(持有目标类引用,在调用前后插入附加逻辑)
public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target = target; } @Override public void addUser(String username) { System.out.println("〖日志〗开始执行addUser,参数:" + username); // 前置增强 target.addUser(username); // 调用真实业务 System.out.println("〖日志〗addUser执行完成"); // 后置增强 } @Override public void deleteUser(String username) { System.out.println("〖日志〗开始执行deleteUser,参数:" + username); target.deleteUser(username); System.out.println("〖日志〗deleteUser执行完成"); } }
静态代理的三大致命缺陷:
类膨胀(Class Explosion) :100个Service接口就需要手写100个代理类-1;
维护噩梦:接口增加或修改方法,代理类和实现类都必须同步修改-1;
重复劳动:所有代理类中的增强逻辑高度相似,却要在每个方法里重复写。
一句话总结:静态代理的本质是人写的“替身类”,必须在编译期就确定代理关系,扩展性极差-2。
既然手写代理类太繁琐,能不能让程序在运行时自动帮我们生成代理类?这就是动态代理要解决的问题。
二、JDK动态代理:运行时生成代理类的“造物主”
2.1 定义与核心角色
JDK动态代理(Java Dynamic Proxy)是Java原生支持的代理技术,允许在运行时动态创建代理类并加载到JVM中-1。
核心角色只有两个-1:
InvocationHandler接口:定义了代理对象被调用时“要做什么”的统一处理入口;Proxy类:提供静态方法newProxyInstance()来生成代理对象。
2.2 生活化类比
JDK动态代理 = “正规中介公司” :必须持有目标对象的“营业执照”(接口)。中介公司(代理对象)本身不是目标对象,但它持有目标对象的所有营业执照(实现了相同接口)。客户拿着营业执照找中介办业务,中介先做前置操作,再通过授权书(反射)呼叫真正的目标对象,最后做后置操作,把结果交给客户-48。
2.3 代码示例
// 1. 定义InvocationHandler:统一拦截入口 public class JdkDynamicProxy implements InvocationHandler { private final Object target; public JdkDynamicProxy(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.nanoTime(); Object result = method.invoke(target, args); // 反射调用真实方法 System.out.println("〖日志〗方法执行耗时:" + (System.nanoTime() - start) + " ns"); return result; } } // 2. 使用Proxy.newProxyInstance生成代理对象 UserService target = new UserServiceImpl(); JdkDynamicProxy handler = new JdkDynamicProxy(target); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 目标类实现的接口数组 handler // 调用处理器 ); proxy.addUser("张三");
2.4 核心机制与限制
必须基于接口:JDK动态代理只能代理实现了接口的类,原因是代理类会继承
Proxy类,而Java不支持多继承,所以只能通过实现接口来扩展-;三个参数缺一不可:
ClassLoader、接口数组、InvocationHandler实例,接口数组必须全是真实实现的接口,不可含类或抽象类-;所有方法调用都路由到
invoke:代理对象的方法调用最终都会被统一转发到InvocationHandler.invoke(),需要在此方法中通过method.invoke(target, args)手动转发到真实对象-2。
三、CGLIB动态代理:打破接口限制的“克隆人工厂”
3.1 定义与核心角色
CGLIB动态代理(Code Generation Library)是一个基于ASM字节码生成框架的第三方代理技术,可以在运行时动态生成目标类的子类作为代理对象-21-47。
核心组件包括-21:
Enhancer:生成器,负责配置父类、接口、拦截器,并生成字节码;MethodInterceptor接口:拦截器,核心拦截入口,处理所有非final方法的调用;MethodProxy:快速调用器,相比JDK反射性能更优。
3.2 生活化类比
CGLIB = “高科技克隆人工厂” :不需要“营业执照”(接口),只要有DNA(类结构)就行。克隆工厂直接提取目标类的DNA(字节码),瞬间克隆出一个长得一模一样的子类(如UserService$$EnhancerByCGLIB$$...)。这个克隆人继承了父类,并且重写了所有非final方法。客户来找目标对象,实际见到的是克隆人,克隆人先做前置操作,然后通过super直接调用父类方法-48。
3.3 代码示例
// 注意:目标类可以没有接口,但类和方法不能是final public class UserService { public void addUser(String username) { System.out.println("数据库新增用户:" + username); } } // 使用CGLIB生成代理 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); // 设置父类(关键步骤) enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> { System.out.println("〖日志〗方法:" + method.getName() + " 开始执行"); long start = System.nanoTime(); Object result = proxy.invokeSuper(obj, args); // 注意是invokeSuper,不是method.invoke System.out.println("〖日志〗方法执行耗时:" + (System.nanoTime() - start) + " ns"); return result; }); UserService proxy = (UserService) enhancer.create(); proxy.addUser("李四");
⚠️ 常见错误:在intercept()方法中必须使用methodProxy.invokeSuper(),而不是method.invoke()。前者通过字节码直接调用父类方法,能绕过代理链避免无限递归;后者是反射调用,如果处理不当会造成死循环-22。
3.4 核心限制
不能代理final类:CGLIB基于继承生成子类,final类无法被继承;
不能代理final/private/static方法:这些方法无法被子类重写或访问,拦截会失败;
构造器无法拦截:因为代理对象是子类实例,父类构造器仍会被调用。
四、JDK vs CGLIB:一张表看懂所有区别
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 实现原理 | 基于接口,运行时生成实现了接口的代理类 | 基于继承,运行时生成目标类的子类 |
| 依赖条件 | 目标类必须实现接口 | 目标类和方法不能是final |
| 依赖库 | Java原生支持,无需第三方库 | 需要cglib库(Spring Core已内置) |
| 生成类名格式 | $Proxy0、$Proxy1 | Target$$EnhancerByCGLIB$$hash |
| 代理对象类型 | instanceof Service为true,instanceof ServiceImpl为false | instanceof ServiceImpl为true |
| 方法调用方式 | 通过反射转发调用 | 通过MethodProxy.invokeSuper()直接调用 |
| 性能特点 | 生成对象快,方法调用因反射略慢 | 首次生成较慢(字节码生成),方法调用效率更高 |
| 适用场景 | 有接口、轻量级、避免第三方依赖 | 无接口、需要代理内部方法调用 |
关于性能的重要说明:在JDK 8及更高版本中,JDK动态代理和CGLIB的性能差距已显著缩小。现代JVM对字节码优化良好,两者的执行效率在大多数场景下差异不大,不要仅凭“性能”做选择-47。
五、底层原理:字节码生成与反射的协同
5.1 JDK动态代理底层机制
JDK动态代理的本质是动态生成字节码 + 反射机制的结合-。
当调用Proxy.newProxyInstance()时,底层发生以下过程-16:
ProxyGenerator在内存中动态生成代理类的字节码(类名格式为$ProxyN);通过
ClassLoader.defineClass()将字节码加载到JVM;创建代理类实例并返回。
生成的代理类会继承Proxy类,并实现所有指定的接口。当你调用代理对象的方法时,代理类内部将所有方法调用统一路由到你自定义的InvocationHandler.invoke()方法中,从而允许你在方法执行前后插入自定义逻辑-16。
5.2 CGLIB底层机制
CGLIB底层使用的是ASM字节码操作库,逐指令构建类文件-23:
它会复制目标类的所有非final方法的字节码;
在方法开头和结尾插入拦截逻辑(类似前置/后置增强);
构造器被重写,第一行强制调用父类构造器,并将
Callback实例存储到字段中(如this.CGLIB$CALLBACK_0)。
两者的底层关系:JDK动态代理是组合关系(代理类持有目标对象,通过接口方法转发);CGLIB是继承关系(代理类是目标类的子类,通过重写父类方法实现拦截)-21。
六、高频面试题与参考答案
面试题1:JDK动态代理为什么只能代理接口?
参考答案(建议背诵):JDK动态代理生成的代理类会继承java.lang.reflect.Proxy类。由于Java是单继承语言,代理类无法再继承其他类,因此只能通过实现接口来扩展功能。所以,JDK动态代理要求目标类必须实现一个或多个接口,否则无法生成代理对象。如果传入非接口类型,Proxy.newProxyInstance()会直接抛出IllegalArgumentException-。
面试题2:JDK动态代理和CGLIB动态代理的本质区别是什么?
参考答案:
| 区别点 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 代理关系 | 组合(代理类持有目标对象) | 继承(代理类是目标类的子类) |
| 调用方式 | 反射调用(method.invoke) | 直接调用(MethodProxy.invokeSuper) |
| 对目标类的要求 | 必须有接口 | 不能是final类,方法不能是final/private |
| 依赖 | Java原生 | 需要cglib库 |
面试题3:Spring AOP默认使用哪种动态代理?
参考答案:Spring Framework默认使用JDK动态代理。从Spring 3.2开始,如果目标类没有实现接口,会自动切换到CGLIB。而Spring Boot 2.x把默认值改成了CGLIB-54。可通过spring.aop.proxy-target-class=true强制统一使用CGLIB-。
面试题4:CGLIB能代理final方法吗?为什么?
参考答案:不能。CGLIB通过继承目标类并重写其非final方法来实现代理。如果方法是final的,子类无法重写该方法,因此CGLIB无法拦截final方法的调用。强行代理final方法会抛出IllegalArgumentException: Cannot subclass final class-22-23。
面试题5:你在项目中如何选择JDK动态代理和CGLIB?
参考答案:
优先使用JDK动态代理:目标类已实现接口、追求轻量级、避免引入第三方依赖时;
选择CGLIB:目标类没有实现接口、需要代理类的内部方法调用、使用Spring Boot 2.x默认配置时;
核心原则:有接口优先JDK(符合面向接口编程原则),无接口则CGLIB-。
七、结尾总结
本文系统梳理了Java代理模式的完整知识链路:
✅ 静态代理:编译期确定代理关系,实现简单但扩展性差,接口增加时代码线性膨胀;
✅ JDK动态代理:运行时基于接口生成代理类,Java原生支持,但必须有接口;
✅ CGLIB动态代理:运行时基于继承生成子类,无接口限制,但不能代理final类和方法;
✅ 底层原理:JDK是“组合+反射”,CGLIB是“继承+字节码生成”;
✅ 选型建议:有接口优先JDK,无接口或用Spring Boot 2.x时选CGLIB。
面试高频考点:JDK只能代理接口的原因(继承Proxy单继承限制)、CGLIB不能代理final方法的原因(继承无法重写)、Spring AOP的默认代理策略、两种代理的性能差异与选型。
延伸阅读预告:下篇文章将深入AOP(面向切面编程),讲解如何利用动态代理实现声明式事务、日志记录和性能监控,并对比Spring AOP与AspectJ的区别。欢迎持续关注!