从零开始理解 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 层添加日志
- 新建 Spring Boot 项目,引入
spring-boot-starter-aop。 - 定义接口和实现类:
// 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"用户详细信息";}}- 编写切面:
@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;}}- 启动类测试:
@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,从而替换掉原始对象。
流程可以总结为:
- Spring 启动,扫描所有切面。
- 创建
UserServiceImpl的 Bean。 -
BeanPostProcessor发现该 Bean 有匹配的切面,决定要为它生成代理。 -
DefaultAopProxyFactory.createAopProxy() 判断UserServiceImpl 实现了UserService 接口 → 选用JdkDynamicAopProxy。 -
JdkDynamicAopProxy.getProxy() 调用Proxy.newProxyInstance,生成$Proxy类实例,并注入所有拦截器链。 - 最终容器存放的是这个代理对象,而不是原始的
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 源码的那天,一定会想起今天这个“经纪人”和“经纪人中介”的故事。