动态代理学习笔记: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。这样代理类不依赖接口,适用范围更广。

相关推荐
小码哥_常1 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
学长毕设4 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
努力的小雨6 小时前
给 AI 眼镜开发应用有多难?我从零做了一个陪练,还上架了
后端
excel7 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
AI入职记8 小时前
AI 代码审查召回率实测:20 个骗过单元测试的变异体,抓到 13 个
后端
浮链序8 小时前
别让 AI 客服把用户当猴耍:百来行 Python 把「转人工」做成一条高可用链路
后端
属于自己的天空8 小时前
Claude Code 插件完全指南:官方市场怎么用,怎么自己写(v2.1.269+)
后端
Tokenfree8 小时前
给大模型调用加一个熔断器:60 行代码,三个阈值
后端
Jason_chen8 小时前
Linux 进程调度器深入解析与实时性优化指南(基于 Linux 5.10+)
后端
136096757238 小时前
三个 Agent 干同一个活,职责边界到底该怎么划
后端