Skip to content

JDK 动态代理和 CGLIB 平时在 Spring AOP、MyBatis 里天天用,但"为什么有接口用 JDK、无接口用 CGLIB"、"两者到底差在哪"一直没系统梳理过。这篇把动态代理从头捋清:为什么要它、两种实现的机制差异、以及选型。

一、为什么需要动态代理

先说静态代理的痛点。代理模式的核心是"包一层代理,前后加逻辑"(见《代理模式》),但静态代理每个接口都要手写一个代理类

java
// 代理 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 你指定的接口

java
// 目标必须实现接口
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 方法有三个参数:

java
Object invoke(Object proxy, Method method, Object[] args);
//              proxy 当前代理对象    method 被调用的方法    args 方法参数

生成的代理类长什么样(理解"为什么必须接口"的关键):

JDK 在运行时生成的代理类,结构大致是:

java
// 反编译看到的代理类结构(示意)
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 目标类),靠"继承"来冒充:

java
// 目标类:没有实现任何接口
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();

MethodInterceptorintercept 方法有四个参数:

java
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy);
//               obj 代理对象(子类实例)   method 被调用的方法   args 参数   proxy 方法代理

最容易踩的坑:invokeSuper vs invoke

java
proxy.invokeSuper(obj, args);   // ✅ 正确:调父类(真实)方法
proxy.invoke(obj, args);        // ❌ 错误:会无限递归,最终 StackOverflowError
  • invokeSuper:调用"代理类重写后的方法对应的父类实现"(即真实逻辑),这是我们要的
  • invoke:直接调用目标方法,但这个调用又会触发拦截器 → 又进 intercept → 又调 invoke → ……无限递归,直接栈溢出

这是 CGLIB 新手几乎必踩的坑——两个方法名只差一个单词,写错了程序直接 StackOverflowError,排查半天才发现是这里。

为什么 final 不行:CGLIB 靠"生成子类 + 重写方法"来代理。final 类不能被继承(生成不了子类)、final 方法不能被重写(插不进拦截逻辑),所以 CGLIB 代理不了 final。这是字节码层面的硬限制,不是 CGLIB 不想支持。

关键点:CGLIB 靠"继承生成子类"来伪装,所以不需要接口——只要目标类能被继承就行。

JDK 动态代理与 CGLIB:一个靠实现接口冒充,一个靠继承生成子类冒充

四、两者对比

维度JDK 动态代理CGLIB
冒充方式实现接口(implements)继承生成子类(extends)
前提目标必须有接口目标可无接口
局限无接口的类代理不了final 类/方法代理不了
来源JDK 内置第三方库(Spring 已集成)
性能生成快,调用略慢(反射)生成慢,调用快(字节码)

一句话记忆:JDK 代理 = "实现接口冒充"(得有接口这个身份证),CGLIB = "继承生成子类冒充"(没身份证,靠儿子装爸爸)

五、实际应用

  • Spring AOP@Transactional 事务、切面,底层就是动态代理——目标有接口用 JDK,无接口用 CGLIB(见《AOP 面向切面编程》)
  • MyBatis MapperUserMapper 只有接口没有实现类,就是 JDK 动态代理生成的(见《MyBatis 快速入门》)
  • RPC 框架:本地代理代表远程服务

小结

  • 动态代理解决"静态代理每个类手写一个代理"的痛点,运行时自动生成
  • JDK 代理靠"实现接口"冒充,目标必须有接口
  • CGLIB 靠"继承生成子类"冒充,无接口也行,但 final 不行
  • Spring AOP 两者都用:有接口用 JDK,无接口用 CGLIB 兜底

想了解"代理"这个设计思想本身(为什么要有代理、静态代理怎么手写),看《代理模式》;想了解动态代理在 Spring 里的应用,看《AOP 面向切面编程》。