动态代理是 Java 运行时在不修改源码的前提下为对象生成代理、织入横切逻辑(日志、事务、权限、监控)的核心机制。理解 JDK Proxy 与 CGLIB 的差异,是看懂 Spring AOP、MyBatis 映射、RPC 框架底层的关键。本文用可运行示例把两者的原理、性能与选型一次讲透。
一、为什么需要动态代理
很多横切逻辑(比如方法耗时统计、事务开启/提交、接口鉴权)和”业务代码”本身无关,却要散落在成百上千个方法里。如果硬编码,维护成本极高。动态代理把这部分逻辑抽出来,在”调用真实对象之前/之后”统一织入,业务类保持干净。
典型场景:Spring 的事务增强、MyBatis 的 Mapper 接口映射、Dubbo/Feign 的远程调用、Glide/Hibernate 的懒加载。它们底层几乎都逃不开 JDK 动态代理或 CGLIB 二选一。
二、JDK 动态代理:基于接口的代理
JDK 动态代理是 JDK 标准库自带的能力,核心是 java.lang.reflect.Proxy 和 InvocationHandler 接口。它要求被代理的对象必须实现接口,代理类会在运行时动态生成一个实现了该接口的类。
2.1 先定义接口与实现
public interface UserService {
String getName(int id);
}
public class UserServiceImpl implements UserService {
@Override
public String getName(int id) {
return "user-" + id;
}
}
2.2 编写增强逻辑(InvocationHandler)
import java.lang.reflect.*;
public class LogHandler implements InvocationHandler {
private final Object target;
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
Object result = method.invoke(target, args); // 调用真实对象
long cost = (System.nanoTime() - start) / 1000;
System.out.println(method.getName() + " 耗时 " + cost + " us");
return result;
}
}
2.3 生成并使用代理
public class Demo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class<?>[]{UserService.class}, // 只能代理接口
new LogHandler(target));
System.out.println(proxy.getName(1));
}
}
注意:Proxy.newProxyInstance 的第二个参数传的是接口数组,因此拿到的是接口类型,只能强转成接口,不能强转成实现类。
三、CGLIB:基于继承的代理
CGLIB(Code Generation Library)通过继承目标类、在运行时用 ASM 生成子类来代理,因此不需要接口。Spring、Hibernate 在目标类没有接口时会自动改用 CGLIB。
3.1 MethodInterceptor 与 Enhancer
import net.sf.cglib.proxy.*;
public class LogInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
long start = System.nanoTime();
Object result = proxy.invokeSuper(obj, args); // 调用父类(真实)方法
long cost = (System.nanoTime() - start) / 1000;
System.out.println(method.getName() + " 耗时 " + cost + " us");
return result;
}
}
public class Demo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new LogInterceptor());
UserServiceImpl proxy = (UserServiceImpl) enhancer.create(); // 强转成类
System.out.println(proxy.getName(2));
}
}
关键点:CGLIB 代理的是类,所以目标和增强方法都不能是 final,否则无法生成子类,会直接抛异常。
四、JDK Proxy 与 CGLIB 对比
| 维度 | JDK 动态代理 | CGLIB |
| 实现机制 | 反射,运行时生成接口代理类 | 继承,ASM 生成子类字节码 |
| 是否需接口 | 必须实现接口 | 不需要,基于类即可 |
| final 限制 | 无(接口方法) | 类与方法不能是 final |
| 代理类型转换 | 只能转成接口 | 可转成目标类 |
| 创建开销 | 较小 | 较大(要生成/加载字节码) |
| 调用开销 | JDK 8+ 已大幅优化 | 早期更快,新版本差距很小 |
| 依赖 | JDK 内置,零依赖 | 需引入 cglib(Spring 已内嵌) |
五、它们都用在哪里
最经典的落地是 Spring AOP:当 Bean 实现了接口,Spring 默认用 JDK 动态代理;否则用 CGLIB。事务注解 @Transactional 的本质就是 Spring 用代理在方法前后开启/提交事务。这也解释了为什么很多”事务不生效”的坑,根因是自调用绕过了代理——同类方法内部调用带注解的方法,代理根本没介入(详见 《Spring 事务失效实战:8 个不回滚的坑》)。
另一个常见组合是 Java SPI 与代理/包装模式:框架常通过动态代理把第三方实现包装成统一接口(参考 《Java SPI 实战:用 ServiceLoader 构建可插拔架构》)。再往外看,《Java 注解处理器实战》 做的是编译期增强,正好和这里的运行期代理形成互补。
六、常见坑与最佳实践
| 坑 | 现象 | 解法 |
| 自调用失效 | 同类内调用代理方法,增强丢失 | 通过 AOP 代理对象调用,或把方法拆到另一个 Bean |
| 类型转换异常 | ClassCastException | JDK 只能转接口;CGLIB 才能转成具体类 |
| CGLIB 构造执行两次 | 构造函数副作用执行两次 | 构造函数保持无副作用,逻辑放其他方法 |
| 频繁创建代理 | 高并发下性能下降 | 缓存代理实例,避免每次 newProxyInstance |
| 被代理方法抛异常 | invoke 内未处理 | 在 InvocationHandler 中统一 try/catch 与日志 |
七、一句话选型建议
如果你的类本来就有接口,优先用 JDK 动态代理(零依赖、调用性能在新版本已足够好);如果没有接口、或必须代理具体类,再上 CGLIB。实际项目中多数情况下不用手动选择——Spring 会根据目标类型自动决定,你只需要理解”为什么它会这么选”以及”事务/缓存为什么突然不生效”。
小结:动态代理是 Java 生态的基石能力。JDK Proxy 胜在”零依赖、基于接口”,CGLIB 胜在”无需接口、基于继承”。掌握两者的边界与坑,不仅能写出更干净的可插拔代码,更能一眼看穿框架底层那些”神奇”的自动增强是怎么发生的。




