Java之动态代理 DynamicProxy
动态代理概念
在学习动态代理之前,先了解静态代理,可参考静态代理
静态代理核心缺陷
- 类爆炸:一个接口实现类,必须手写一个专属代理类,业务类越多,代理类文件成倍增加;
- 增强逻辑无法复用:日志、耗时统计等通用逻辑在每个代理类重复复制,修改需要改动全部代理;
- 接口迭代维护成本极高:接口新增方法,所有业务类、全部静态代理类必须同步重写,极易漏改;
- 运行时无法灵活适配:代理类编译前手写固定,无法根据运行时参数动态切换代理目标。
动态代理的核心设计目标
不用手动编写代理类,程序运行阶段通过反射 / 字节码技术自动生成代理对象,统一拦截所有目标方法,一套增强逻辑适配同一接口下全部实现类,彻底解决静态代理所有短板。
动态代理两大分类
| 类型 | 依赖 | 限制条件 | 底层原理 | 适用场景 |
|---|---|---|---|---|
| JDK 动态代理 | JDK 原生反射包 | 目标类必须实现接口 | 运行时生成实现目标接口的代理类 | 有接口的 Service、Mapper、Feign 远程接口 |
| CGLIB 动态代理 | 第三方字节码包(spring 内置) | 目标类不能是 final,方法不能 final | 运行时生成目标类的子类,重写业务方法 | 无接口的普通业务类 |
本文章主要以JDK 动态代理讲解
动态代理核心概念
JDK 动态代理依靠java.lang.reflect反射包实现,运行时动态生成实现目标接口的匿名代理类;所有代理对象的方法调用,都会统一转发至InvocationHandler的invoke方法拦截,开发者在该方法统一编写前置 / 后置增强逻辑。
三大核心 API
(1)InvocationHandler拦截处理器(核心)
public interface InvocationHandler { /** * 代理方法统一拦截入口 * @param proxy 自动生成的代理对象 * @param method 当前被调用的目标方法反射对象 * @param args 调用方法传入的参数数组 * @return 方法执行返回值 */ Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }invoke参数讲解:
proxy:动态生成的代理对象本身,极少使用,仅用于区分代理实例;method:本次触发调用的方法反射对象,唯一标识当前执行的业务方法;args:调用方法传入的参数,无参方法则为null。
(2)Proxy代理生成工具类
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)ClassLoader loader:类加载器,使用目标对象的类加载器,用于加载动态生成的代理类;Class<?>[] interfaces:目标类实现的接口数组,代理类会全部实现这些接口,保证多态互换;InvocationHandler h:自定义拦截处理器,绑定到代理对象,所有方法调用转发至该处理器。
(3)Method反射方法对象
封装当前调用的方法信息:方法名、参数、返回值、所属类,用于反射执行真实目标方法。
结合代码彻底理解
package demo1.basic; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Scanner; public class DynamicAgent { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("===== 动态代理测试菜单 ====="); System.out.println("1:Kobe 2:Jordan 3:pyLiu"); System.out.print("请选择球员:"); int choice = scanner.nextInt(); NBA target = null; switch (choice) { case 1: target = new Kobe(); break; case 2: target = new Jordan(); break; case 3: target = new pyLiu(); break; default: System.out.println("无效选择"); return; } // 绑定拦截处理器 DogSon dogSon = new DogSon(); dogSon.getTarget(target); NBA proxy = (NBA) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), dogSon ); // ========== 测试1:无参无返回值方法 personalAction() ========== System.out.println("\n---------- 调用无参方法 personalAction ----------"); proxy.personalAction(); // ========== 测试2:多参数、无返回值方法 score(int, String) ========== System.out.println("\n---------- 调用双参数无返回方法 score ----------"); proxy.score(81, "季后赛单场最高分"); // ========== 测试3:带参数、有返回值方法 getCareerData(int) ========== System.out.println("\n---------- 调用带参有返回方法 getCareerData ----------"); String data = proxy.getCareerData(2010); System.out.println("【主线程接收方法返回值】" + data); } // 自定义拦截处理器 static class DogSon implements InvocationHandler { private NBA target; void getTarget(NBA target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // ===================== 1、区分不同方法,差异化前置增强 ===================== if ("personalAction".equals(methodName)) { // 无参方法,args = null if (target instanceof Kobe) { System.out.print("What can I say "); } else if (target instanceof Jordan) { System.out.print("GOAT "); } else if (target instanceof pyLiu) { System.out.print("你抽中了隐藏款!! "); } } else if ("score".equals(methodName)) { // 带参方法:打印args数组,查看参数传递效果 System.out.print("【score方法前置】当前传入参数:"); for (Object arg : args) { System.out.print(arg + " "); } System.out.print(" | 球员:" + target.getClass().getSimpleName() + " "); } else if ("getCareerData".equals(methodName)) { System.out.print("【getCareerData方法前置】查询年份:" + args[0] + " "); } // 打印目标类名 System.out.println(" ===== " + target.getClass().getSimpleName()); // ===================== 2、反射执行业务方法 ===================== // 统一一行代码适配:无参/多参、有返回/无返回所有方法 Object result = method.invoke(target, args); // ===================== 3、后置增强:处理返回值 ===================== if (result != null) { System.out.println("【后置增强】业务方法返回内容:" + result); } else { System.out.println("【后置增强】该方法无返回值(void)"); } return result; // 把业务返回值向上传递给调用方 } } // 统一业务接口:3种方法覆盖 无参void、多参void、带参有返回值 interface NBA { // 无参数、无返回值 void void personalAction(); // 多个参数、无返回值 void void score(int point, String desc); // 单个参数、有返回值 String String getCareerData(int year); } // Kobe 实现类 static class Kobe implements NBA { @Override public void personalAction() { System.out.println("湖人总冠军"); } @Override public void score(int point, String desc) { System.out.println("科比拿下" + point + "分,记录:" + desc); } @Override public String getCareerData(int year) { if (year == 2010) { return "2010年科比拿下总冠军+FMVP"; } return "科比生涯数据"; } } // Jordan 实现类 static class Jordan implements NBA { @Override public void personalAction() { System.out.println("黑人乔丹"); } @Override public void score(int point, String desc) { System.out.println("乔丹拿下" + point + "分,记录:" + desc); } @Override public String getCareerData(int year) { return "乔丹6冠,历史第一人"; } } // pyLiu 实现类 static class pyLiu implements NBA { @Override public void personalAction() { System.out.println("暴扣乔丹"); } @Override public void score(int point, String desc) { System.out.println("我大学室友" + point + "分,记录:" + desc); } @Override public String getCareerData(int year) { return "上理第一给"; } } }(建议粘贴到idea查看,观察输入输出)
主要针对三种不同方案进行分别讲解
公共前置流程
- 程序启动,控制台输入选择
1 - 创建被代理对象:
target = new Kobe() - 创建拦截处理器实例:
DogSon dogSon = new DogSon() - 将真实球员对象注入处理器:
dogSon.getTarget(target) - 调用
Proxy.newProxyInstance(),JVM 在内存自动生成动态代理类$Proxy0,返回代理对象proxy
走完以上步骤后,分三条独立链路执行无参方法、有参方法、带返回值方法。
(1)无参方法
- 主线执行
proxy.personalAction() - 自动生成的代理类
$Proxy0捕获该方法调用,封装三组参数:- proxy:当前代理实例
- method:对应
personalAction()的反射方法对象 - args:
null(无入参)
- 执行
dogSon.invoke(proxy, method, null)进入拦截逻辑 - invoke 内部逻辑:
- 通过
method.getName()判断当前是personalAction,打印Kobe前置文案 - 打印目标类名
Kobe - 执行反射调用
method.invoke(target, null),触发Kobe.personalAction(),输出业务内容 - 该方法是 void,反射执行结果为
null,后置打印「无返回值」 - return null 逐层回传给主线程,主线无变量接收
- 通过
(2)有参方法
- 主线执行
proxy.score(81, "季后赛单场最高分") - 代理类捕获调用,封装三组参数:
- proxy:当前代理实例
- method:对应
score(int,String)的反射对象 - args:
new Object[]{81, "季后赛单场最高分"}(入参封装成数组)
- 执行
dogSon.invoke(proxy, method, args) - invoke 内部逻辑:
- 通过方法名识别为
score,循环遍历 args 数组打印所有传入参数 - 打印目标类名
Kobe - 执行
method.invoke(target, args),数组内参数自动传给业务方法,执行Kobe.score(81, "季后赛单场最高分") - void 方法,执行结果为 null,后置打印「无返回值」
- return null 回传给主线程
- 通过方法名识别为
(3)有返回值方法
- 主线执行
String data = proxy.getCareerData(2010) - 代理类捕获调用,封装三组参数:
- proxy:当前代理实例
- method:对应
getCareerData(int)的反射对象 - args:
new Object[]{2010}
- 执行
dogSon.invoke(proxy, method, args) - invoke 内部逻辑:
- 通过方法名识别为
getCareerData,读取args[0]获取查询年份打印前置日志 - 打印目标类名
Kobe - 执行
method.invoke(target, args),调用Kobe.getCareerData(2010),得到返回字符串2010年科比拿下总冠军+FMVP - 后置增强打印业务返回内容
- 将字符串结果
return向上传递
- 通过方法名识别为
- 主线程接收返回值,存入变量
data,打印输出
个人学习问题解惑
1、代理类proxy在调用方法(如.personalAction())时,是如何把方法名传给拦截器(DogSon)中的invoke函数中的method?简单来说就是,我拦截器的invoke是怎么知道要执行personalAction()这个方法的?
通过查询,在调用(NBA) Proxy.newProxyInstance时,会在内存自动生成一个实现NBA接口的代理类,这个类是看不见的,伪代码如下:
// JDK自动生成的代理类 $Proxy0 implements NBA public class $Proxy0 implements NBA { // 构造时传入你写的DogSon拦截器 private final InvocationHandler h; public $Proxy0(InvocationHandler h) { this.h = h; } // 重写接口无参方法 personalAction @Override public void personalAction() { try { // 1. 提前预加载好当前方法的Method对象(固定写死在代理类字节码里) Method m = NBA.class.getDeclaredMethod("personalAction"); // 2. 直接把method、代理对象、参数打包传给invoke h.invoke(this, m, null); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } // 重写接口有参方法 score @Override public void score(int point, String desc) { try { Method m = NBA.class.getDeclaredMethod("score", int.class, String.class); // 调用时传入的81、"季后赛单场最高分" 自动封装成数组args Object[] args = new Object[]{point, desc}; h.invoke(this, m, args); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } // 重写带返回值方法 getCareerData @Override public String getCareerData(int year) { try { Method m = NBA.class.getDeclaredMethod("getCareerData", int.class); Object[] args = new Object[]{year}; // 接收invoke返回的结果,向上返回给主线程 Object res = h.invoke(this, m, args); return (String) res; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } }可以看到,隐式的代理类$Proxy0重写了NBA接口的所有方法,显式的Proxy在构造时将拦截器InvocationHandler传给了隐式的代理类$Proxy0,而$Proxy0中在每个方法里都指明了Method,并在调用拦截器的invoke方法时将this(也就是Proxy),method,args都传给了拦截器中的invoke方法用以执行。
以上内容纯属个人经验帖,欢迎讨论,查漏补缺。
