news 2026/5/8 17:12:31

从零开始理解 JDK 动态代理:保姆级教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始理解 JDK 动态代理:保姆级教程

从零开始理解 JDK 动态代理:保姆级教程

1. 生活中的“代理”——先理解代理模式

你有没有找过明星合影?通常你没法直接联系到明星本人,而是通过经纪人。你把自己的请求(合影)告诉经纪人,经纪人判断是否能打扰明星,然后再转达给明星,最后由明星完成合影,经纪人可能还会加一句“注意形象,保持微笑”。

在这个场景里:

  • — 调用方(客户端)
  • 经纪人— 代理
  • 明星— 真实对象(目标对象)

代理的作用就是在访问真实对象之前或之后,添加一些额外的操作(比如过滤请求、记录日志、控制权限),但最终还是要调用真实对象的方法。


2. 静态代理:每个经纪人只能服务一个明星?

在编程里,最原始的代理做法是静态代理:为每一个需要被代理的类,都单独手写一个代理类。

比如有一个接口Star

publicinterfaceStar{voidsing();}

真实明星类:

publicclassRealStarimplementsStar{@Overridepublicvoidsing(){System.out.println("明星本人在唱歌~");}}

静态代理类:

publicclassStarProxyimplementsStar{privateStarrealStar;// 持有真实明星publicStarProxy(StarrealStar){this.realStar=realStar;}@Overridepublicvoidsing(){System.out.println("经纪人:确认场地,收钱");realStar.sing();// 调真实明星唱歌System.out.println("经纪人:安排粉丝见面会");}}

使用:

StarrealStar=newRealStar();Starproxy=newStarProxy(realStar);proxy.sing();

这样做有什么问题?
假设你又有Actor​、Dancer​ 等接口,每个都要写一个对应的代理类,代码会无限膨胀,无法复用。有没有一种方法能自动生成代理类,不用手工为每个接口重复写逻辑呢?

——于是,动态代理诞生了。


3. JDK 动态代理是什么?

JDK 动态代理是 Java 原生提供的一种机制,可以在程序运行时(而不是编译时)动态地创建出一个代理对象,这个对象会实现你指定的接口,并将所有方法调用转发到一个统一的处理器上。

它的核心组件只有两个:

  • java.lang.reflect.Proxy:用来生成代理对象的工厂类。
  • java.lang.reflect.InvocationHandler​:接口,里面只有一个方法invoke,你需要在这里写好「拦截逻辑」,也就是代理要做的那些额外事情。

关键约束:JDK 动态代理只能代理实现了接口的类,无法直接代理一个没有实现任何接口的普通类。
(如果你需要代理普通类,可以用 CGLIB 这类第三方库,不是本篇重点,最后会简单对比)


4. 手把手写一个 JDK 动态代理

我们还是用明星的例子,但这次用一个通用的日志记录代理,任何接口都能复用。

4.1 定义接口和实现类

// 明星接口publicinterfaceStar{voidsing(StringsongName);Stringdance();}
// 真实明星:只会唱歌跳舞publicclassRealStarimplementsStar{@Overridepublicvoidsing(StringsongName){System.out.println("本明星唱了一首:"+songName);}@OverridepublicStringdance(){System.out.println("本明星跳了一支热舞!");return"街舞";}}

4.2 编写InvocationHandler—— 代理的“大脑”

这个处理器就是你的经纪人逻辑。它需要实现InvocationHandler​ 接口,并实现invoke方法:

importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;publicclassLogHandlerimplementsInvocationHandler{privateObjecttarget;publicLogHandler(Objecttarget){this.target=target;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{System.out.println("【日志】准备调用方法:"+method.getName());if(args!=null&&args.length>0){System.out.println("【日志】参数:"+java.util.Arrays.toString(args));}Objectresult=method.invoke(target,args);System.out.println("【日志】方法调用完毕,返回值:"+result);returnresult;}}

4.3 使用Proxy.newProxyInstance创建代理对象

importjava.lang.reflect.Proxy;publicclassMain{publicstaticvoidmain(String[]args){StarrealStar=newRealStar();LogHandlerhandler=newLogHandler(realStar);StarproxyStar=(Star)Proxy.newProxyInstance(realStar.getClass().getClassLoader(),newClass[]{Star.class},handler);proxyStar.sing("夜曲");System.out.println("----------");Stringresult=proxyStar.dance();System.out.println("最终拿到的返回值:"+result);}}

运行结果:

【日志】准备调用方法:sing 【日志】参数:[夜曲] 本明星唱了一首:夜曲 【日志】方法调用完毕,返回值:null ---------- 【日志】准备调用方法:dance 本明星跳了一支热舞! 【日志】方法调用完毕,返回值:街舞 最终拿到的返回值:街舞

5. 原理探秘:生成的代理类长什么样?

JDK 动态代理在运行时会生成一个形如$Proxy0的类,类似这样(伪代码):

publicfinalclass$Proxy0extendsProxyimplementsStar{public$Proxy0(InvocationHandlerh){super(h);}@Overridepublicvoidsing(StringsongName){super.h.invoke(this,m3,newObject[]{songName});}@OverridepublicStringdance(){return(String)super.h.invoke(this,m4,null);}}

由于 Java 是单继承,代理类已经继承了Proxy,所以只能实现接口——这就是 JDK 动态代理必须基于接口的原因。


6. JDK 动态代理的常见应用场景

6.1 AOP(面向切面编程)

比如你想给一个项目中所有 Service 的方法都加上事务管理、日志或耗时统计。不用挨个改类,用动态代理一包裹就行。

🔧 Spring AOP 实战:给 Service 层添加日志
  1. 新建 Spring Boot 项目,引入spring-boot-starter-aop
  2. 定义接口和实现类
// UserService.javapublicinterfaceUserService{voidlogin(Stringusername);StringgetUserInfo(Longid);}
// UserServiceImpl.javaimportorg.springframework.stereotype.Service;@ServicepublicclassUserServiceImplimplementsUserService{@Overridepublicvoidlogin(Stringusername){System.out.println(username+" 登录成功!");}@OverridepublicStringgetUserInfo(Longid){System.out.println("查询用户 id = "+id);return"用户详细信息";}}
  1. 编写切面
@Aspect@ComponentpublicclassLogAspect{@Around("execution(* com.example.demo.service.UserService.*(..))")publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{StringmethodName=joinPoint.getSignature().getName();Object[]args=joinPoint.getArgs();System.out.println("【Spring日志】调用方法:"+methodName);Objectresult=joinPoint.proceed();System.out.println("【Spring日志】方法执行完毕,返回值:"+result);returnresult;}}
  1. 启动类测试
@SpringBootApplicationpublicclassDemoApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(DemoApplication.class,args);UserServiceuserService=context.getBean(UserService.class);userService.login("小白");Stringinfo=userService.getUserInfo(1001L);System.out.println("最终获得的用户信息:"+info);System.out.println("代理对象类型:"+userService.getClass().getName());}}

输出:

【Spring日志】调用方法:login 小白 登录成功! 【Spring日志】方法执行完毕,返回值:null 【Spring日志】调用方法:getUserInfo 查询用户 id = 1001 【Spring日志】方法执行完毕,返回值:用户详细信息 代理对象类型:com.sun.proxy.$Proxy50

可以看到,Spring 容器里的UserService​ bean 实际是一个 JDK 动态代理对象。那么问题来了:Spring 具体是怎么生成这个代理对象的?


🔍 Spring 底层是如何创建 JDK 动态代理的?(源码浅析)

Spring AOP 的代理生成过程可以拆成这几个步骤,我用类比来理解:

1. Spring 的“经纪人中介”——DefaultAopProxyFactory

在 Spring 内部,有一个专门“决定用哪种代理方式”的工厂,叫DefaultAopProxyFactory​。它的主要方法是createAopProxy(AdvisedSupport config)​。它会根据被代理的目标对象是否实现了接口来做判断:

  • 如果目标对象实现了至少一个接口 → 创建JdkDynamicAopProxy(JDK 动态代理)
  • 否则 → 创建CglibAopProxy(CGLIB 代理)

这就像一个中介,看你有没有“接口营业执照”——有的话走 JDK 通道,没有就走 CGLIB 通道。

2. 代理的“实际执行者”——JdkDynamicAopProxy

JdkDynamicAopProxy​ 这个类同时实现了AopProxy​ 接口和InvocationHandler​ 接口。它是 Spring 用作 JDK 动态代理的关键。它内部有一个getProxy()​ 方法,就是在这个方法里调用了熟悉的Proxy.newProxyInstance

简化后的关键源码长这样:

publicObjectgetProxy(@NullableClassLoaderclassLoader){// 获取目标对象实现的接口Class<?>[]proxiedInterfaces=AopProxyUtils.completeProxiedInterfaces(this.advised,true);// 调用 Java 原生方法创建代理对象,this 就是 InvocationHandlerreturnProxy.newProxyInstance(classLoader,proxiedInterfaces,this);}

这和我们自己手写LogHandler​ 时的做法如出一辙,只不过JdkDynamicAopProxy​ 的invoke方法里会执行一系列“通知”(前置通知、后置通知、环绕通知等),而不仅仅是简单地打日志。

3. 代理创建的“触发时机”——AbstractAutoProxyCreator

Spring 怎么知道哪些 Bean 需要被代理?在 Bean 的生命周期中,有一个特殊的BeanPostProcessor​ 叫做AbstractAutoProxyCreator​(具体子类如AnnotationAwareAspectJAutoProxyCreator​)。它在 Bean 初始化之后会调用wrapIfNecessary()​ 方法,检查这个 Bean 是否需要被增强(比如有没有匹配的切面)。如果需要,就会使用上述的DefaultAopProxyFactory来创建代理对象,并返回代理 bean,从而替换掉原始对象。

流程可以总结为:

  1. Spring 启动,扫描所有切面。
  2. 创建UserServiceImpl的 Bean。
  3. BeanPostProcessor发现该 Bean 有匹配的切面,决定要为它生成代理。
  4. DefaultAopProxyFactory.createAopProxy()​ 判断UserServiceImpl​ 实现了UserService​ 接口 → 选用JdkDynamicAopProxy
  5. JdkDynamicAopProxy.getProxy()​ 调用Proxy.newProxyInstance​,生成$Proxy类实例,并注入所有拦截器链。
  6. 最终容器存放的是这个代理对象,而不是原始的UserServiceImpl

所以,你在测试代码里通过context.getBean(UserService.class)​ 拿到的,正是这个自动生成的代理对象,它实现了UserService​ 接口,内部通过JdkDynamicAopProxy​ 的invoke方法调度所有切面逻辑和真实方法。


6.2 RPC 框架(远程过程调用)

在 Dubbo 等框架中,客户端只拿到一个接口,通过动态代理生成的代理对象,调用任意方法时实际是发送网络请求到远程服务器,并返回结果。你完全感受不到网络的存在。

6.3 MyBatis 的核心实现

MyBatis 我们只写 Mapper 接口,不写实现类,为什么能执行 SQL?
实际上 MyBatis 用 JDK 动态代理为每个 Mapper 接口生成了代理对象,当你调用mapper.findById(1)​ 时,代理的invoke方法会根据接口全限定名和方法名找到对应的 SQL 并执行。

6.4 各种拦截器、权限控制

可以在invoke方法里面,先判断是否有权限,再决定是否执行真实方法。


7. 总结与注意事项

优点

  • 无需手动编写代理类,复用便捷
  • 将横切逻辑(日志、事务)与业务逻辑解耦
  • Java 原生支持,零依赖
  • Spring AOP 默认借助它优雅地实现了声明式切面

⚠️局限

  • 只能代理接口(被代理的类必须至少实现一个接口)
  • 如果代理没有接口的普通类,Spring 会自动转用 CGLIB,但原生 JDK 方式会报错
  • 代理内部通过反射调用,有一定性能开销(现代 JVM 优化后影响很小)

延伸对比:CGLIB

  • CGLIB 通过继承目标类来生成子类代理,可代理无接口的类。
  • 不能代理final​ 类或final方法。
  • Spring AOP 默认优先 JDK 动态代理,无接口时切换 CGLIB。

8. 动手做一做

建议亲自实践:

  • 把例子中UserServiceImpl​ 的接口去掉,看控制台代理类名是否变成 CGLIB 的$$EnhancerBySpringCGLIB
  • 给原生LogHandler增加计时功能。
  • 查源码阅读JdkDynamicAopProxy​ 的invoke方法,看它如何执行通知链。

希望这篇“大白话”教程让你对 JDK 动态代理以及它在 Spring 中的工作原理有了透彻的理解。当你看懂 Spring AOP 或 MyBatis 源码的那天,一定会想起今天这个“经纪人”和“经纪人中介”的故事。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/8 17:11:56

工业物联网软件集成实战:DDS与OPC UA融合架构解析

1. 工业物联网系统集成的核心挑战与演进 在工业自动化领域摸爬滚打了十几年&#xff0c;我亲眼见证了系统集成重心的巨大转变。早期&#xff0c;我们这些工程师的精力几乎全耗在硬件上&#xff1a;如何把不同厂商的PLC&#xff08;可编程逻辑控制器&#xff09;通过Profibus或M…

作者头像 李华
网站建设 2026/5/8 17:11:40

如何将 4K 视频从 iPhone 传输到 PC/ Mac ?

4K 视频是一种高清视频格式&#xff0c;分辨率为 3840 x 2160 像素&#xff0c;是标准高清视频的四倍。虽然 4K 视频能够提供更细腻的图像和逼真的色彩&#xff0c;但它也占用更多存储空间。如果您用 iPhone 拍摄了太多 4K 视频&#xff0c;可能需要将 4K 视频从 iPhone 传输到…

作者头像 李华
网站建设 2026/5/8 17:11:30

FModel:如何用3个步骤轻松掌握虚幻引擎游戏资源提取技巧

FModel&#xff1a;如何用3个步骤轻松掌握虚幻引擎游戏资源提取技巧 【免费下载链接】FModel Unreal Engine Archives Explorer 项目地址: https://gitcode.com/gh_mirrors/fm/FModel 想要深入了解你喜爱的虚幻引擎游戏内部构造吗&#xff1f;FModel作为一款专业的虚幻引…

作者头像 李华
网站建设 2026/5/8 17:09:47

创业公司如何通过Taotoken低成本接入多模型支持产品迭代

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 创业公司如何通过Taotoken低成本接入多模型支持产品迭代 对于资源有限的创业公司而言&#xff0c;在产品中集成智能对话功能是一个…

作者头像 李华
网站建设 2026/5/8 17:09:11

从跑分到体验:DPD多尔蒂架构下射频功放器件选型与设计实战

1. 从“跑分”到“体验”&#xff1a;现代基站功放设计的范式转移十年前&#xff0c;如果有人问我&#xff0c;选一颗射频功率LDMOS管&#xff0c;最看重什么参数&#xff1f;我会毫不犹豫地回答&#xff1a;效率、增益、带宽。这就像在车展上挑跑车&#xff0c;谁的马力大、零…

作者头像 李华
网站建设 2026/5/8 17:09:06

如何高效使用开源ZXP安装器:一站式Adobe插件管理解决方案

如何高效使用开源ZXP安装器&#xff1a;一站式Adobe插件管理解决方案 【免费下载链接】ZXPInstaller Open Source ZXP Installer for Adobe Extensions 项目地址: https://gitcode.com/gh_mirrors/zx/ZXPInstaller 还在为Adobe插件安装的繁琐流程而烦恼吗&#xff1f;ZX…

作者头像 李华