Java 动态代理实战:JDK 与 CGLIB 对比

动态代理是 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
类型转换异常ClassCastExceptionJDK 只能转接口;CGLIB 才能转成具体类
CGLIB 构造执行两次构造函数副作用执行两次构造函数保持无副作用,逻辑放其他方法
频繁创建代理高并发下性能下降缓存代理实例,避免每次 newProxyInstance
被代理方法抛异常invoke 内未处理在 InvocationHandler 中统一 try/catch 与日志

七、一句话选型建议

如果你的类本来就有接口,优先用 JDK 动态代理(零依赖、调用性能在新版本已足够好);如果没有接口、或必须代理具体类,再上 CGLIB。实际项目中多数情况下不用手动选择——Spring 会根据目标类型自动决定,你只需要理解”为什么它会这么选”以及”事务/缓存为什么突然不生效”。

小结:动态代理是 Java 生态的基石能力。JDK Proxy 胜在”零依赖、基于接口”,CGLIB 胜在”无需接口、基于继承”。掌握两者的边界与坑,不仅能写出更干净的可插拔代码,更能一眼看穿框架底层那些”神奇”的自动增强是怎么发生的。

上一篇 K8s 配置与密钥:ConfigMap/Secret 实战
下一篇 TIME_WAIT 过多排查实战:从端口耗尽到连接复用优化
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]