一文说透"静态代理"与"动态代理"
追溯
学一个技术,要知道技术因何而产生,才能有学下去的目标和动力,才能更好的理解
另外,收藏的老哥,也动动手给点个赞呗,收藏都超`50`多了,还没一个赞,哭辽~
首先,要明确为什么要存在代理
呢?
存在一个常见的需求:怎样在不修改类A代码的情况下,在调用类A的方法时进行一些功能的附加与增强呢?
先不考虑什么代理不代理的,我们设计一个简单的实现方案:
新创建一个类B,类B组合类A,在类B中创建一个方法b,方法b中调用类A中的方法a,在调用前和调用后都可以添加一些自定义的附加与增强代码。 当有需求需要调用类A的方法a并且想要添加一个附加功能时,就去调用类B的方法b即可实现上述需求;
下面为了便于理解,附上伪代码:
下面,让我们来调用一下ClassB的methodb方法,则会产生以下输出:
可以发现,方法a执行了,并且在没有修改类A代码的前提下,为方法a附加了其他的功能;
不难吧,其实上述的代码就是一个最简单的代理模式
了
代理存在的意义:使用代理模式可以在不修改别代理对象代码的基础上,通过扩展代理类,进行一些功能的附加与增强
代理种类
代理分为静态代理
和动态代理
,其涉及的设计模式就是代理模式
本尊了,代理模式一般包含几种元素,如下图:
主题接口(subject):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
真实主题(RealSubject):真正实现业务逻辑的类;
代理类(Proxy):用来代理和封装真实主题;
客户端(Client):使用代理类和主题接口完成一些工作。
为了更好的理解,我们将上述实现的最简易版的代理完善一下,添加接口,代理类也实现相应的被代理类的接口,实现同一个方法,伪代码如下:
静态代理
所谓静态代理也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
上面的代码就是实现了一个静态代理; 其实静态代理就已经能够满足上述需求了,为什么还需要动态代理呢? 这里就涉及到静态代理的两个缺点了
代理对象
的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,在程序规模稍大时静态代理代理类就会过多会造成代码混乱如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度。
基于上述两个问题,动态代理诞生了~
动态代理
动态代理是在程序运行时,通过反射获取被代理类的字节码内容用来创建代理类
具体什么是动态代理呢?
名词:动态
,动态在程序中就是表达在程序运行时就根据配置自动的生成代理类并且代理类和被代理类是在运行时才确定相互之间的关系;
在JDK中包含两种动态代理的实现机制:JDK Proxy
和 CGLib
;
下面我们以JDK Proxy
为例,讲解一下动态代理和根据源码分析并简单说一下应用场景
JDK Proxy
JDK Proxy动态代理
,api在包java.lang.reflect
下,大家可能发现了,为什么在反射的包下呢?这个问题我们下面的源码分析会解决;
其核心api包含两个重要的核心接口和类:一个是 InvocationHandler(Interface)
、另一个则是 Proxy(Class)
,简单说就这两个简单的很,这两个是我们实现动态代理所必需的用到的,下面简单介绍一下两个类:
java.lang.reflect.Proxy(Class)
:Proxy是 Java 动态代理机制的主类,提供一组静态方法来为一组接口动态地生成代理类及其对象。包含以下四个静态方法:
static InvocationHandler getInvocationHandler(Object proxy)
该方法用于获取指定代理对象所关联的调用处理器
static Class getProxyClass(ClassLoader loader, Class[] interfaces)
该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
static boolean isProxyClass(Class cl)
该方法用于判断指定类对象是否是一个动态代理类
static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h)
该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例,包含下面的参数:
* loader
指定代理类的ClassLoader加载器
* interfaces
指定代理类要实现的所有接口
* h
: 表示的是当这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上
该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
java.lang.reflect.InvocationHandler(interface)
: InvocationHandler
是上述newProxyInstance
方法的InvocationHandler h
参数传入,负责连接代理类和委托类的中间类必须实现的接口
它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
上述就是动态代理两个核心的方法,不太明白?先别急,我们先用上述实现一个动态代理,你先看一下
还是以上述的案例从静态代理来改造为动态代理,实现动态代理主要就两步,假设还是存在上述的ImplA、ClassA
1:创建一个处理器
类实现InvocationHandler接口,重写invoke方法,伪代码如下:
好了,这样一个处理器
就搞定了,当我们在调用被代理类
的方法时,就是去执行上述重写的invoke方法
,下面创建一个ClassA的代理类
**2:创建代理类
,并调用被代理方法**
好了,至此一个动态代理就构建完成了,执行代码,会发现输出:
太简单了有木有,这里总结一下动态代理的优缺点:
优点:
动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。
动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
接口增加一个方法,除了所有实现类需要实现这个方法外,动态代理类会直接自动生成对应的代理方法。
缺点:
JDK proxy只能对有实现接口的类才能代理,也就是说没有接口实现的类,jdk proxy是无法代理的,为什么呢?下面会解答.
有什么解决方案吗? 当然有,还有一种动态代理的方案:CGLib
,它是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,对于final类或方法,是无法继承的,和jdk proxy基本思想是相似的,毕竟都是动态代理的实现方案嘛,在这篇文章就不做详解了,博主会在其他的博文单独介绍这个nb的框架
上述带大家搞了一遍动态代理和静态代理的应用;在这过程中,你有没有想过,动态代理是怎么实现的呢?
下面我们就从源码的角度分析一下,解决大家的疑问。
源码分析
在开始分析的时候,我希望大家带着几个问题
去阅读,可以帮助大家更好的理解:
问题1
:代理类为什么可以在运行的时候自动生成呢?如何生成的呢?问题2
:为什么调用代理类的相应的代理方法就可以调用到InvocationHandler实现类的invoke方法呢?问题3
:为什么jdk proxy只支持代理有接口实现的类呢?
ps :为了提升阅读体验,让大家有一个更清晰的认知,以下源码会将一些异常处理和日志打印代码删除,只保留主干代码,请知悉~
我们就从两个核心:InvocationHandler
和Proxy
来进行分析整个脉络,他们都在java.lang.reflect
包下
InvocationHandler源码
上述就是InvocationHandler的源码,没什么其他的就是一个接口,里面有一个待实现方法invoke,处理类实现此接口重写invoke方法
Proxy源码
Proxy
类的整体的架构就类似于上述,InvocationHandler h
参数和两个构造函数
、四个上述已经介绍过的共有方法
,还有一系列的私有方法,getProxyClass、isProxyClass、getInvocationHandler功能就和上面介绍的一样,就不再详细介绍了
**我们下面来主要看一下newProxyInstance
方法**
newProxyInstance方法,我在方法内添加上了对应的注释:
在上面的代码中,我简单的标注了一下每行代码的作用,下面我们来详细分析一下;
代理类的字节码生成逻辑
我们知道,在加载jvm前,java文件都已经被编译成了class字节码
文件, 然后jvm通过类加载器
将字节码文件加载到jvm中;
我们的代理类也是这样,不同的是动态代理的类是在程序运行时产生的,我们要做的就是如何在程序运行的时候,通过被代理类
的字节码生成代理类
的字节码!
我们接下来详细分析newProxyInstance
方法:
在newProxyInstance中调用了Class<?> cl = getProxyClass0(loader, intfs);
语句生成了代理类的字节码
,此处调用了getProxyClass0方法,传入了指定的类加载器和对应要实现的接口
那么, 我们看看getProxyClass0
方法的实现:
proxyClassCache
是Proxy类中的静态变量,是WeakCache类,里面封装了两个类KeyFactory、ProxyClassFactory,都是BiFunction函数式接口(如果不清楚函数式接口,请自行google);
将其拿过来看一下proxyClassCache = new WeakCache<>(new KeyFactory(),new ProxyClassFactory());
其中调用了proxyClassCache.get(loader, interfaces)方法的实现
未避免代码过长,只粘贴了核心代码:
总结一下上述方法的流程:
接着ProxyClassFactory.apply
方法看一下:
上述的 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
为生成代理类字节码数组的方法,调用的方法中调用了generateClassFile
方法;
在generateClassFile
中,你会发现里面全部是重组字节码的代码, 主要是获取被代理类
字节码和操作类InvocationHandler
字节码组装出代理类
的字节码,在重组的过程因为是在运行时
进行了代理类的创建,无法像往常一样new一个被代理类的实例获取他的方法,让代理类进行调用。
获取字节码后,接下来就要将代理类的字节码加载进JVM中了,这里调用的是一个return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length)
其中的defineClass0
是一个本地native 方法,传入了代理类名称、类加载器、代理类的字节码文件、文件长度参数
,从而将字节码加载进JVM中! 代码如下:
将代理类的字节码加载进JVM后,会在方法区内生成一个Class对象
,标识这个代理类;
代理类的实例生成逻辑
上面,我们知道了通过字节码技术生成了代理类字节码,并通过类加载器将字节码文件加载到了JVM的方法区中生成了一个Class对象,我们如何在运行时获取这个Class对象的实例呢? 只有获取了对象实例才可以使用不是~
还是回到newProxyInstance
方法中,上面我们分析了Class<?> cl = getProxyClass0(loader, intfs)
这部分逻辑,生成了Class对象cl
,下面生辰该实例代码,过程很简单,相关逻辑我就直接在代码中注释了
上述就是生成实例的代码,生成实例后newProxyInstance
就返回该实例了,就可以使用了~
反射:在运行时获取被代理类的字节码
那如何才能在运行时获取到被代理类的构造函数、方法、属性等字节码呢? 此时“反射!
”登场了!我们通过反射可以在运行时获取到类的所有信息,所有哦。
定义: JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
。
比如,在上述所说的组装代理类字节码时,在获取被代理类的所有方法时,就调用了Method[] var5 = var4.getMethods();
反射中的getMethods
方法,通过反射获取到了被代理类的所有方法,这样我们就可以在运行时获取到任何类的所有的字节码信息了! 从而可以组装出我们想要的代理类字节码!
所以说,反射
也为动态代理
的实现提供了理论支持
!!因为只有在运行时能获取到对应类的信息,才可以通过信息创造出对应的我们所需要的代理类;
源码分析总结
总而言之,动态代理
的理论支持是可以通过反射机制
在运行时
获取到类的所有信息,如果运行时获取不到被代理类的信息,那还咋生成代理类。
动态代理的大致流程:
通过上述流程。我们就获得了一个代理类对象了,调用代理类对应的方法,就会执行我们规定的执行逻辑,实现对被代理类的运行时动态增强和扩展!
此时,我们再拿出刚开始我们用JDK proxy实现的动态代理代码中的生成代理类的代码:ImplA proxyA = (ImplA)Proxy.newProxyInstance(A.getClass().getClassLoader(), A.getClass().getInterfaces(), myHandler)
每个参数的作用,是不是就很清晰了
问题解答
上面动态代理实现流程
,我们可以回答上述的第一个代理类为什么可以在运行的时候自动生成呢?如何生成的呢?
问题了
对于第二个为什么调用代理类的相应的代理方法就可以调用到InvocationHandler实现类的invoke方法呢?
和第三个为什么jdk proxy只支持代理有接口实现的类呢?
问题,我们需要反编译一下我们通过字节码技术产生的代理类
,如下:
上述代码包含几个关键点:
方法为
final
类型,不可再被继承代理名称为
$Proxy
代理类前缀 + 递增数字继承动态代理的核心类
Proxy
, 实现了我们指定的接口ImplA
一个带参构造方法
$Proxy0(InvocationHandler var1)
传入InvocationHandler内部调用了父类的Proxy
的构造函数methoda、toString、hashCode、equals全部调用的传入的InvocationHandler参数的
invoke
方法!!!
现在回答第二个问题为什么调用代理类的相应的代理方法就可以调用到InvocationHandler实现类的invoke方法呢?
显而易见,代理类内部的代理方法全部显式调用的InvocationHandler实现类的invoke方法
第三个问题为什么jdk proxy只支持代理有接口实现的类呢?
因为代理类在使用JDK proxy方式生成代理类时,默认继承Proxy类,又因为java语言是单继承不支持多继承,那怎样才能标识我要代理什么类型的类或是代理什么方法呢? 接口呗,java支持接口的多继承,多少个都ok~
好了,上述将动态代理的使用方式 和 实现原理统一过了一遍,也回答了几个容易疑惑的问题,下面我们简单说下动态代理在现实的java框架大家庭中的一些典型应用
动态代理的应用
**spring aop
** : 这可以说是spring框架中最典型的应用了,通过动态代理在运行时产生代理类,完成对被代理类的增强和功能附加
**RPC框架的实现
** : 远程过程调用,RPC使得调用远程方法和调用本地方法一样,这是怎么搞的呢?服务方对外放出服务的接口api,调用方拿到接口api,通过动态代理的方式生成一个代理类,代理类的处理类的invoke方法可以通过websocket连接远程服务器调用对应的远程接口; 这样我们再用代理对象进行调用对应方法时时,就像调用本地方法一样了
**mybatis框架中
** : mapper.xml中编写sql语句,mapper.java接口写上对应的方法签名;我们直接调用mapper.java中的方法就可以执行对应的sql语句,有没有想过为什么? 框架使用动态代理创建一个mapper.java的代理对象,代理对象的处理类invoke中执行sql,就ok了
总结
代理分为静态代理
和动态代理
,动态代理的两种实现方式:JDK Proxy
和CGLib
,动态代理的核心反射机制
,通过反射在运行时获取被代理类字节码和处理类字节码,动态代理代理类的生成通过重组字节码
的方式。
原创不易,有收获的话,关注
、点赞
、评论
三连支持,我最大的动力~
关于博文有任何问题请不吝评论,感谢
参考:JDK源码,https://www.jianshu.com/p/861223789d53
版权声明: 本文为 InfoQ 作者【洋仔聊编程】的原创文章。
原文链接:【http://xie.infoq.cn/article/8114fba36dae232639f1c6efa】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论