详解Java动态代理(JDK版)以及Invoke的调用

由于Kotlin没有内置提供动态代理,一般用的都是java原生的动态代理来操作,所以我们主要讲原生的代理方式也就是常说的JDK动态代理

由于本文章主要讲动态代理部分,时间有限,所以不会再讲一遍代理是什么,可自行去查资料补下基础知识

java环境

JDK17

引言

我们知道代理的设计模式可以让我们在不修改类结构的情况下做一些额外的事情,经典操作比如方法调用前后的日志打印,在spring中基于代理思想可以使用spring提供的方式进行aop操作等。在代理中又分动态和静态代理,在静态代理中我们需要提前编写代理类,如果需要多个不同操作的代理,那么这个时候是非常麻烦的,所以衍生出了动态代理,帮你动态的创建代理类,不需要你提前编写。减少了大量的代理类编写操作,(spring中获取的bean都是基于动态代理生成的,所以内置支持aop操作)

题外话:在springboot中默认已经使用Cglib进行动态代理,但是Spring还是默认使用原生的代理方式 相关issue

通过动态代理获取一个代理对象

创建一个动态的代理对象我们通过Proxy.newProxyInstance来创建,传入类的加载器和他实现的接口数组 还有他代理类绑定的InvocationHandler的实现。

主要通过InvocationHandler中的invoke方法,当代理类实现的接口中的函数被调用时,会回调这个方法,在这个方法中获取相关是哪一个方法被调用了然后手动执行方法的invok操作传入实现类的对象进行方法调用,这样就可以统一管理被代理类中方法(也就是接口中的方法)调用的时机。从而实现相关的方法调用前后的操作

在这段代码中核心就是通过Proxy.newProxyInstance方法创建动态代理类,我们就从此方法开始分析

newProxyInstance做了什么

1. InvocationHandler的空检查

 

java

代码解读

复制代码

Objects.requireNonNull(h); //判断是否传入的InvocationHandler是空的如果是空的则抛出异常 public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; }

2. 安全管理器的获取

在jdk17中,SecurityManager已经被弃用了, 所以System.getSecurityManager()通常是null,并且这块与动态代理没啥关系,可以不用关心

 

java

代码解读

复制代码

@SuppressWarnings("removal") final Class<?> caller = System.getSecurityManager() == null? null: Reflection.getCallerClass();

3. 获取代理对象的构造器

注意这里获取的不是被代理对象的构造器,是动态代理生成的那个代理对象的构造器

 

java

代码解读

复制代码

Constructor<?> cons = getProxyConstructor(caller, loader, interfaces); // 这里传入的 caller就是上文的安全管理器,上文说了 通常是null,认为是null就行 不影响后续越读 // loader就是被代理类的类加载器 // interfaces是被代理类实现的接口 //上述的loader和interfaces都是我们newProxyInstance的时候传入的 private static Constructor<?> getProxyConstructor(Class<?> caller, ClassLoader loader, Class<?>... interfaces) { //下面段的if主要就是对不同的个数实现的interface进行适配 /** * proxyCache.sub(intf).computeIfAbsent(xxxxx), * 通过传入不同数量的接口,从缓存中构建相关的ClassLoaderValue * 通过ClassLoaderValue的computeIfAbsent传入对应代理类的ClassLoader来获取之前关联的代理对象的构造器缓存 * 如果不存在则通过new ProxyBuilder(ld, clv.key()).build()重新构建一个代理对象构造器存进缓存中并且返回 * */ if (interfaces.length == 1) { Class<?> intf = interfaces[0]; if (caller != null) { checkProxyAccess(caller, loader, intf); } return proxyCache.sub(intf).computeIfAbsent( loader, (ld, clv) -> new ProxyBuilder(ld, clv.key()).build() ); } else { final Class<?>[] intfsArray = interfaces.clone(); if (caller != null) { checkProxyAccess(caller, loader, intfsArray); } final List<Class<?>> intfs = Arrays.asList(intfsArray); return proxyCache.sub(intfs).computeIfAbsent( loader, (ld, clv) -> new ProxyBuilder(ld, clv.key()).build() ); } }

4. 创建代理对象

传入代理对象构造函数和InvocationHandler创建代理对象并且返回

 

java

代码解读

复制代码

// 这里的caller和上面说的一样 看作null 不影响 // cons就是代理对象的构造函数 // InvocationHandler newProxyInstance中创建的InvocationHandler对象 return newProxyInstance(caller, cons, h);

这里面核心的就是 cons.newInstance(new Object[]{h}); 通过代理对象的构造函数创建对应的代理对象 然后传入InvocationHandler,这样一个代理对象就创建完了

汇总

newProxyInstance 实际上就是获取缓存中关联的代理对象构造函数,如果不存在则创建,然后通过代理对象的构造函数来创建对应的代理对象,就这么简单,帮我们自动的创建被代理类的代理对象。

invoke的调用在哪?

现在就衍生一个问题,为什么我们调用代理对象的函数的时候能触发我们InvocationHandler中的invoke呢?

我们上面说了 我们获取的代理对象实际上不是单纯的实现的接口,而是通过一个单独的代理对象构造函数来生成的,

通过断点们会发现我们的代理对象实际上是$Proxy0类型的一个对象,

我们可以通过 System.getProperties()["jdk.proxy.ProxyGenerator.saveGeneratedFiles"] = "true" 来获取我们动态代理生成的代理类的class文件

在我们的代理类中我们发现他不单单是实现了IUser的接口,而是还继承了Proxy。

并且在代理对象的构造函数中会发现需要传入的刚好就是我们写的那个InvocationHandler 他调用了super的构造函数传入了我们的InvocationHandler, 这里的super也就是继承的Proxy。

在Proxy中我们能看到这么一段代码 

 我们会发现Proxy中会保存我们传入的InvocationHandler,

 回到代理类中我们会发现他对所有的方法都进行了重写

在任何方法执行的时候实际上执行的不是原有的内容,而是直接调用我们的InvocationHandler.invoke来传入代理类还有当前被调用的方法的定义还有方法的参数, 所以实际上方法的执行场所是在invoke中,这样就能让我们在invoke被调用的时候获取当前执行的函数然后决定是否调用和调用前后的相关操作

总结

所以动态代理实际上是通过使用newProxyInstance来传入我们被代理类的加载器和实现接口用来创建相关联的代理类构造器,并且会有一个代理类构造器缓存,当重复对同一个被代理类进行动态的代理对象创建的时候会从缓存中获取之前创建的对应的代理类的构造对象来使用。

我们的代理对象的构造器创建出来的代理类实际上还继承了Proxy,他用于保存我们的InvocationHandler,并且代理类的内部对实现的接口和本身的相关方法都进行了重写,并且获取了所有方法的创建了对应的属性, 执行代理类中所有的方法的时候,实际上执行的流程是调用保存的InvocationHandler的invoke方法,然后传入对应的方法声明和相关参数和代理类,这样就能在invoke中手动决定方法的调用,从而实现invoke中监听到方法的调用并且可以在方法调用的前后做相关的操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值