JDK 动态代理和 CGLIB 平时在 Spring AOP、MyBatis 里天天用,但"为什么有接口用 JDK、无接口用 CGLIB"、"两者到底差在哪"一直没系统梳理过。这篇把动态代理从头捋清:为什么要它、两种实现的机制差异、以及选型。
一、为什么需要动态代理
先说静态代理的痛点。代理模式的核心是"包一层代理,前后加逻辑"(见《代理模式》),但静态代理每个接口都要手写一个代理类:
// 代理 UserService
public class UserServiceProxy implements UserService {
private UserService target;
public void save() {
log("前置"); target.save(); log("后置");
}
}
// 代理 OrderService —— 又要手写一遍几乎一样的代码
public class OrderServiceProxy implements OrderService { ... }
// 代理 PayService —— 再来一遍 ...一个项目几十个 Service,就要手写几十个几乎雷同的代理类,纯体力活。动态代理解决的就是这个:不用手写,运行时自动生成代理类。
二、JDK 动态代理:靠"实现接口"冒充
JDK 内置了 java.lang.reflect.Proxy,能在运行时动态生成一个类,这个类 implements 你指定的接口:
// 目标必须实现接口
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
public void save() { System.out.println("保存用户"); }
}
// 动态代理:运行时生成一个 implements UserService 的代理类
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(), // ① 类加载器:加载"运行时生成的代理类"用
new Class[]{ UserService.class }, // ② 接口数组:代理类要实现哪些接口
(p, method, args) -> { // ③ InvocationHandler:核心回调
System.out.println("前置");
Object result = method.invoke(new UserServiceImpl(), args); // 反射调真实方法
System.out.println("后置");
return result;
}
);
proxy.save();三个参数各管一件事:
- 类加载器:加载"运行时生成的代理类"(通常传目标类的类加载器)
- 接口数组:代理类要
implements哪些接口——这就是"靠接口冒充"的来源 - InvocationHandler:核心回调,代理类的每个方法调用都会进
invoke,前后逻辑 + 反射调真实方法都在这里做
InvocationHandler 的 invoke 方法有三个参数:
Object invoke(Object proxy, Method method, Object[] args);
// proxy 当前代理对象 method 被调用的方法 args 方法参数生成的代理类长什么样(理解"为什么必须接口"的关键):
JDK 在运行时生成的代理类,结构大致是:
// 反编译看到的代理类结构(示意)
public final class $Proxy0 extends Proxy implements UserService {
// 每个接口方法都重写,内部转发给 handler.invoke
public void save() {
super.h.invoke(this, m3 /* save 方法 */, null); // 转给 InvocationHandler
}
}看到没——代理类已经 extends Proxy 了。Java 是单继承,代理类既然继承了 JDK 内置的 Proxy,就不能再继承目标类,只能靠 implements 接口来冒充。这就是"JDK 代理必须有接口"的深层原因——不是设计偏好,是 Java 单继承逼出来的。
关键点:JDK 生成的代理类是"实现接口"来伪装成目标对象的。既然它要实现接口,目标对象就必须先有接口——没有接口,代理类就"无从冒充"(没法实现一个不存在的接口)。这就是"有接口用 JDK 代理"的根源。
三、CGLIB:靠"继承生成子类"冒充
当目标类没有接口时,JDK 代理就失效了,用 CGLIB。CGLIB 通过字节码生成一个目标类的子类(extends 目标类),靠"继承"来冒充:
// 目标类:没有实现任何接口
public class UserService {
public void save() { System.out.println("保存用户"); }
}
// CGLIB:生成 UserService 的子类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class); // 指定父类(目标类)
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("前置");
Object result = proxy.invokeSuper(obj, args); // 调父类(真实)方法
System.out.println("后置");
return result;
});
UserService proxy = (UserService) enhancer.create();MethodInterceptor 的 intercept 方法有四个参数:
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy);
// obj 代理对象(子类实例) method 被调用的方法 args 参数 proxy 方法代理最容易踩的坑:invokeSuper vs invoke:
proxy.invokeSuper(obj, args); // ✅ 正确:调父类(真实)方法
proxy.invoke(obj, args); // ❌ 错误:会无限递归,最终 StackOverflowErrorinvokeSuper:调用"代理类重写后的方法对应的父类实现"(即真实逻辑),这是我们要的invoke:直接调用目标方法,但这个调用又会触发拦截器 → 又进intercept→ 又调invoke→ ……无限递归,直接栈溢出
这是 CGLIB 新手几乎必踩的坑——两个方法名只差一个单词,写错了程序直接 StackOverflowError,排查半天才发现是这里。
为什么 final 不行:CGLIB 靠"生成子类 + 重写方法"来代理。final 类不能被继承(生成不了子类)、final 方法不能被重写(插不进拦截逻辑),所以 CGLIB 代理不了 final。这是字节码层面的硬限制,不是 CGLIB 不想支持。
关键点:CGLIB 靠"继承生成子类"来伪装,所以不需要接口——只要目标类能被继承就行。
四、两者对比
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 冒充方式 | 实现接口(implements) | 继承生成子类(extends) |
| 前提 | 目标必须有接口 | 目标可无接口 |
| 局限 | 无接口的类代理不了 | final 类/方法代理不了 |
| 来源 | JDK 内置 | 第三方库(Spring 已集成) |
| 性能 | 生成快,调用略慢(反射) | 生成慢,调用快(字节码) |
一句话记忆:JDK 代理 = "实现接口冒充"(得有接口这个身份证),CGLIB = "继承生成子类冒充"(没身份证,靠儿子装爸爸)。
五、实际应用
- Spring AOP:
@Transactional事务、切面,底层就是动态代理——目标有接口用 JDK,无接口用 CGLIB(见《AOP 面向切面编程》) - MyBatis Mapper:
UserMapper只有接口没有实现类,就是 JDK 动态代理生成的(见《MyBatis 快速入门》) - RPC 框架:本地代理代表远程服务
小结
- 动态代理解决"静态代理每个类手写一个代理"的痛点,运行时自动生成
- JDK 代理靠"实现接口"冒充,目标必须有接口
- CGLIB 靠"继承生成子类"冒充,无接口也行,但 final 不行
- Spring AOP 两者都用:有接口用 JDK,无接口用 CGLIB 兜底
想了解"代理"这个设计思想本身(为什么要有代理、静态代理怎么手写),看《代理模式》;想了解动态代理在 Spring 里的应用,看《AOP 面向切面编程》。
