动态代理学习笔记:JDK 与 CGLIB 的完整代码流程

一、为什么需要动态代理

在写业务代码时,经常需要在某个方法执行前后"加料"------比如记录日志、检查权限、开启事务。最笨的办法是写一个静态代理类,手动实现接口的每个方法,在方法里包一层逻辑。但接口一多、方法一多,这种写法就变成了体力活。

动态代理要解决的就是这个问题:不手写代理类,让程序在运行时自动生成。Java 提供了两种主流方案,下面分别梳理完整流程。

二、JDK 动态代理:基于接口的委托机制

完整代码

先看一个最小可运行示例。假设有一个用户服务接口:

java

arduino 复制代码
public interface UserService {
    void addUser(String name);
    String getUserInfo(Long id);
}

实现类:

java

typescript 复制代码
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }

    @Override
    public String getUserInfo(Long id) {
        return "用户信息-" + id;
    }
}

代理处理器(核心):

java

typescript 复制代码
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("=== 方法 " + method.getName() + " 开始 ===");
        // 反射调用真实对象的方法
        Object result = method.invoke(target, args);
        System.out.println("=== 方法 " + method.getName() + " 结束 ===");
        return result;
    }
}

使用:

java

scss 复制代码
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),  // 类加载器
    target.getClass().getInterfaces(),   // 接口数组
    new LogInvocationHandler(target)     // 调用处理器
);

proxy.addUser("张三");
// 输出:
// === 方法 addUser 开始 ===
// 添加用户:张三
// === 方法 addUser 结束 ===

代码执行流程

调用 proxy.addUser("张三") 时,实际发生了什么:

第一步:进入代理类的方法。 JVM 运行时生成的代理类(比如 $Proxy0)实现了 UserService 接口,它的 addUser 方法体里没有任何业务逻辑,只有一行代码:调用 InvocationHandler 的 invoke 方法,并把 Method 对象和参数数组传进去。

第二步:invoke 方法被触发。 这就是上面 LogInvocationHandler.invoke 的入口。三个参数的含义:

参数 含义
proxy 生成的代理对象本身(一般不用)
method 被调用的方法,这里是 UserService.addUser
args 参数数组,这里是 ["张三"]

第三步:手动调用真实对象。 method.invoke(target, args) 通过反射在 target(真实的 UserServiceImpl 实例)上执行 addUser。

第四步:返回结果。 真实方法执行的返回值被传回调用方。

为什么必须基于接口

Proxy.newProxyInstance 的内部逻辑是:拿到接口数组,用 ProxyClassFactory 生成一个类,这个类继承 java.lang.reflect.Proxy,同时实现传入的所有接口 。

Java 不支持多继承,代理类已经继承了 Proxy,就不可能再继承你的实现类。所以它只能通过"实现接口"的方式来代理,真实对象必须作为参数注入进来,靠反射调用------这是一种委托机制 。

三、CGLIB 动态代理:基于继承的拦截机制

完整代码

CGLIB 不需要接口,直接代理类。同样是用户服务,这次去掉接口:

java

typescript 复制代码
public class UserService {
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }

    public String getUserInfo(Long id) {
        return "用户信息-" + id;
    }
}

方法拦截器:

java

typescript 复制代码
public class LogMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) 
            throws Throwable {
        System.out.println("=== 方法 " + method.getName() + " 开始 ===");
        // 注意:这里用 invokeSuper,不是 method.invoke
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("=== 方法 " + method.getName() + " 结束 ===");
        return result;
    }
}

使用:

java

scss 复制代码
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);      // 设置父类
enhancer.setCallback(new LogMethodInterceptor()); // 设置回调
UserService proxy = (UserService) enhancer.create(); // 生成代理对象

proxy.addUser("李四");
// 输出:
// === 方法 addUser 开始 ===
// 添加用户:李四
// === 方法 addUser 结束 ===

代码执行流程

第一步:Enhancer 生成子类。 enhancer.create() 会在运行时创建一个 UserService 的子类,比如 UserService$$EnhancerByCGLIB$$abc123。这个子类重写了父类的所有非 final 方法 。

第二步:调用子类方法。 当你调用 proxy.addUser("李四") 时,实际执行的是子类中重写后的 addUser 方法。

第三步:子类方法委托给拦截器。 重写的方法体内没有业务逻辑,而是调用 MethodInterceptor.intercept(),把当前对象、方法信息、参数、以及一个 MethodProxy 对象传进去。

第四步:invokeSuper 调用父类原始方法。 拦截器里的 proxy.invokeSuper(obj, args) 是关键------它通过 MethodProxy 和 FastClass 机制,直接调用父类(即真实的 UserService)的方法 ,而不是通过反射。

FastClass 解决了什么问题

MethodProxy 背后有一个 FastClass 机制。CGLIB 为每个类生成一个 FastClass 辅助类,里面维护了方法名到索引的映射。调用时通过索引直接定位方法,避免了反射的开销。

为什么不能代理 final 方法

CGLIB 的代理类继承目标类,并重写 方法。final 方法不能被子类重写,所以代理类无法拦截它。调用 final 方法时,会直接执行父类的原始逻辑,拦截器完全不生效 ------这是静默失败,不是报错。

四、两者的核心区别

维度 JDK 动态代理 CGLIB
代理方式 实现接口 继承类,生成子类
真实对象 作为参数注入,靠反射调用 代理类是子类,通过 invokeSuper 调用父类
机制 委托 继承
限制 必须有接口 不能代理 final 类/方法
常用场景 Spring AOP 有接口时 Spring AOP 无接口时

性能方面,JDK 8+ 对反射做了大量优化,高频调用下 JDK 代理和 CGLIB 的差距已经很小,业务系统中通常不需要刻意选择。

五、Spring AOP 怎么选

Spring AOP 的选择逻辑很简单:

text

ini 复制代码
目标类有接口 → JDK 动态代理
目标类没有接口 → CGLIB
配置 proxyTargetClass=true → 强制 CGLIB

Spring Boot 2.x 之后,proxyTargetClass 默认就是 true,所以即使类有接口,Spring 也倾向于用 CGLIB。这样代理类不依赖接口,适用范围更广。

相关推荐
Bazingga1 小时前
Spring AI 还是 LangChain4j?我用同一套业务代码跑了一遍,答案出乎意料
后端
汝吾此便安好1 小时前
Axum 中间件执行顺序
后端
徐小黑ACG2 小时前
Golang 基础06 接口interface
开发语言·后端·golang
三小河2 小时前
从 Markdown 到 Generative UI:AI 如何从“生成答案”进化到“生成界面”?
前端·人工智能·后端
韩振方3 小时前
为什么加了 sudo,写配置文件还是 Permission denied?
后端
牛奔3 小时前
mise 管理多版本 Go 项目
开发语言·前端·chrome·后端·golang
打工仔折腾 AI4 小时前
FaceFusion本地换脸实战:Windows整合包、模型选择与遮罩调参记录
人工智能·windows·后端·python·深度学习·性能优化·ai agent 实战
Nebula_g4 小时前
JavaSE拓展:工具类Executors
java·开发语言·后端·spring·基础·javase
弈栈录4 小时前
Java 后端高并发设计:线程池、限流、熔断与降级
后端·架构