👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主📕系列专栏:Java设计模式、数据结构和算法、kafka从入门到成神、Kafka从成神到升仙、spr
在 Spring
中,最重要的应该当属 ioc
和 aop
了,ioC
的源码流程还比较简单,但 AOP
的流程就较为抽象了。
其中,AOP
中代理模式的重要性不言而喻,但对于没了解过代理模式的人来说,痛苦至极
于是,我就去看了动态代理的实现,发现网上大多数文章讲的都是不清不楚,甚至讲了和没讲似的,让我极其难受
本着咱们方向主打的就是源码,直接从从源码角度讲述一下 代理模式
兄弟们系好安全带,准备发车!
注意:本文篇幅较长,请留出较长时间来阅读
代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
举个生活中常见的例子:客户想买房,房东有很多房,提供卖房服务,但房东不会带客户看房,于是客户通过中介买房。
这时候对于房东来说,不直接和客户沟通,而是交于中介进行代理
对于中介来说,她也会在原有的基础上收取一定的中介费
我们创建 Landlord
接口如下:
public interface Landlord { // 出租房子 void apartmentToRent();}
创建其实现类 HangZhouLandlord
代表杭州房东出租房子
public class HangZhouLandlord implements Landlord { @Override public void apartmentToRent() { System.out.println("杭州房东出租房子"); }}
创建代理类 LandlordProxy
,代表中介服务
public class LandlordProxy { public Landlord landlord; public LandlordProxy(Landlord landlord) { this.landlord = landlord; } public void apartmentToRent() { apartmentToRentBefore(); landlord.apartmentToRent(); apartmentToRentAfter(); } public void apartmentToRentBefore() { System.out.println("出租房前,收取中介费"); } public void apartmentToRentAfter() { System.out.println("出租房后,签订合同"); }}
创建最终测试:
public class JavaMain { public static void main(String[] args) { Landlord landlord = new HangZhouLandlord(); LandlordProxy proxy = new LandlordProxy(landlord); // 从中介进行租房 proxy.apartmentToRent(); }}
得出最终结果:
出租房前,收取中介费杭州房东出租房子出租房后,签订合同
通过上述 demo
我们大概了解代理模式是怎么一回事
动态代理利用了jdk api,动态地在内存中构建代理对象,从而实现对目标对象的代理功能,动态代理又被称为JDK代理或接口代理。
静态代理与动态代理的区别:
class
文件class
文件,而是在运行时动态生成类字节码,并加载到 JVM
中代码如下:
public class ProxyFactory { // 目标方法 public Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { return Proxy.newProxyInstance( // 目标对象的类加载器 target.getClass().getClassLoader(), // 目标对象的接口类型 target.getClass().getInterfaces(), // 事件处理器 new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是前置增强"); method.invoke(target, args); System.out.println("我是后置增强"); return null; } } ); }}
我们测试一下:
public class JavaMain { public static void main(String[] args) { Landlord landlord = new HangZhouLandlord(); System.out.println(landlord.getClass()); Landlord proxy = (Landlord) new ProxyFactory(landlord).getProxyInstance(); proxy.apartmentToRent(); System.out.println(proxy.getClass()); while (true){} }}
得出结果:
class com.company.proxy.HangZhouLandlord我是前置增强杭州房东出租房子我是后置增强class com.sun.proxy.$Proxy0
这里可能有小伙伴已经懵了,接着往后看
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
java.lang.Class
对象,作为方法区这个类的各种数据访问入口由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
从本地获取
从网络中获取
运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy
类中,就是用了 ProxyGenerator.generateProxyClass
来为特定接口生成形式为 *$Proxy
的代理类的二进制字节流
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到 JVM
中使用
所以,我们可以得出一个结论:我们上面的 $Proxy0
实际上是 JVM
在编译时期加载出来的类,由于这个类是编译时期加载的,所以我们没办法在 idea
里面看到。
可能一般的文章,到这里基本就结束了,让大家知道 $Proxy0
是由 JVM
编译时期加载出来的类
但大家都知道,小黄的文章主打的就是一个硬核、源码级。所以,我们直接去看 $Proxy0
的源代码
首先,我们需要下载一个 arthas
的产品,网址:https://arthas.aliyun.com/doc/,跟随流程解压即可。
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、GC、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
当我们一切准备完成后,启动我们上面动态代理的测试 JavaMain
类
启动完成后,进入我们的 arthas
页面,执行命令:java -jar arthas-boot.jar
我们可以看到,我们的目标类 com.company.proxy.JavaMain
就出现了,随后我们按下 4
,进入到我们的监控页面。
随后使用 jad com.sun.proxy.$Proxy0
之后,可以看到我们已经解析出来 $Proxy0
的源码了
我们将其复制到下面,并删减一些不必要的信息。
public final class $Proxy0 extends Proxy implements Landlord { private static Method m3; // $Proxy0 类的构造方法 // 参数为 invocationHandler public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { m3 = Class.forName("com.company.proxy.Landlord").getMethod("apartmentToRent", new Class[0]); } public final void apartmentToRent() { this.h.invoke(this, m3, null); return; }}
我们先看其有参构造方法,可以看到 $Proxy0
的构造方法入参为 InvocationHandler
,有没有感觉似曾相识。
如果你这里忘掉了,不妨去看一下动态代理的 ProxyFactory
的代码,可以发现,我们 Proxy.newProxyInstance()
的第三个自定义的参数,也正是我们的 InvocationHandler
。
我们猜测一下,如果这里的传的 InvocationHandler
是我们之前自定义的 InvocationHandler
那么,如果我调用 $Proxy0.apartmentToRent()
是不是就是执行下面的代码:
public final void apartmentToRent() { this.h.invoke(this, m3, null); return;}// 这里的h.invoke执行的是我们这里自定义的方法,然后进行的前后增强public Object getProxyInstance() { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我是前置增强"); method.invoke(target, args); System.out.println("我是后置增强"); return null; } } );
如果说我们这个猜测是正确的话,那么会得出这样的几个结论:
Landlord
的接口,然后重写了 Landlord
接口中的 apartmentToRent
方法apartmentToRent()
方法时,实际上是调用的我们自定义的 new InvocationHandler()
类里面的 invoke
方法还有我们的最后一步,也就是证明 $Proxy0
的构造入参 InvocationHandler
就是我们自定义的 InvocationHandler
,废话不多说,直接来看代理的源码。
return Proxy.newProxyInstance(ClassLoader,Interfaces,new InvocationHandler() {});public static Object newProxyInstance(ClassLoader loader,Class>[] interfaces,InvocationHandler h){ // cl = class com.sun.proxy.$Proxy0 Class> cl = getProxyClass0(loader, intfs); // cons = public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler) final Constructor> cons = cl.getConstructor(constructorParams); // 根据构造参数实例化对象 return cons.newInstance(new Object[]{h});}
我们通过源码可以看到,一共分为三步(下面为反射的内容,如不熟悉可提前学习下反射):
$Proxy0
的 Class
Class
拿到其构造方法这就确定了我们上述的猜想是正确的。
cglib (Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。cglib
为没有实现接口的类提供代理,为 JDK
的动态代理提供了很好的补充。
ASM
是操作字节码的工具cglib
基于 ASM
字节码工具操作字节码(即动态生成代理,对方法进行增强)SpringAOP
基于 cglib
进行封装,实现 cglib
方式的动态代理使用 cglib
需要引入 cglib
的jar包,如果你已经有 spring-core
的jar包,则无需引入,因为 spring
中包含了cglib
。
cglib
的Maven坐标
cglib cglib 3.2.5
还是同样的配方,我们要创建一个需要代理的类(UserServiceImpl),但不需要实现任何的接口,因为我们的 cglib
是根据类来进行创建的。
UserServiceImpl
public class UserServiceImpl { // 查询功能 List findUserList() { return Collections.singletonList("小A"); }}
实现 cglib
的工厂类:UserLogProxy
public class UserLogProxy implements MethodInterceptor { public Object getLogProxy(Object target) { // 增强器类,用来创建动态代理类 Enhancer enhancer = new Enhancer(); // 设置代理类的父类字节码对象 enhancer.setSuperclass(target.getClass()); // 设置回调 enhancer.setCallback(this); // 创建动态代理对象并返回 return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("前置输出"); Object result = methodProxy.invokeSuper(o, objects); return result; }}
测试程序:JavaMainTest
public class JavaMainTest { public static void main(String[] args) { // 目标对象 UserServiceImpl userService = new UserServiceImpl(); System.out.println(userService.getClass()); // 代理对象 UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService); System.out.println(proxy.getClass()); List list = proxy.findUserList(); System.out.println("用户信息:" + list); while (true) { } }}
结果:
class com.study.spring.proxy.UserServiceImplclass com.study.spring.proxy.UserServiceImpl$$EnhancerByCGLIB$$cd9788d前置输出用户信息:[小A]
按照上述我们分析 $Proxy0
的方法,将 com.study.spring.proxy.UserServiceImpl$$EnhancerByCGLIB$$cd9788d
取出,得到如下:
public class UserServiceImpl$$EnhancerByCGLIB$$cd9788d extends UserServiceImpl implements Factory { final List findUserList() { // 是否设置了回调 MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0; if (methodInterceptor == null) { UserServiceImpl$$EnhancerByCGLIB$$cd9788d.CGLIB$BIND_CALLBACKS(this); methodInterceptor = this.CGLIB$CALLBACK_0; } // 设置回调,需要调用 intercept 方法 if (methodInterceptor != null) { return (List) methodInterceptor.intercept(this, CGLIB$findUserList$0$Method, CGLIB$emptyArgs, CGLIB$findUserList$0$Proxy); } // 无回调,调用父类的 findUserList 即可 return super.findUserList(); } final List CGLIB$findUserList$0() { return super.findUserList(); }}
博主先把整个流程图放到下面,然后结合流程图来进行讲解:
JVM
编译期间,我们的 Enhancer
会根据目标类的信息去动态的生成 动态代理类
并设置 回调
findUserList()
方法时,有两个执行选项 UserLogProxy
中的 intercept
,然后通过 FastClass
类调用动态代理类,执行CGLIB$findUserList$0
方法,调用父类的 findUserList()
方法findUserList()
方法jdk
代理和 CGLIB
代理
使用 CGLib
实现动态代理,CGLib
底层采用 ASM
字节码生成框架,使用字节码技术生成代理类,在JDK1.6
之前比使用 Java
反射效率要高。唯一需要注意的是,CGLib
不能对声明为 final
的类或者方法进行代理,因为 CGLib
原理是动态生成被代理类的子类。
在 JDK1.6
、JDK1.7
、JDK1.8
逐步对 JDK
动态代理优化之后,在调用次数较少的情况下,JDK
代理效率高于 CGLib
代理效率,只有当进行大量调用的时候,JDK1.6
和 JDK1.7
比 CGLib
代理效率低一点,但是到 JDK1.8
的时候,JDK
代理效率高于 CGLib
代理。所以如果有接口使用 JDK
动态代理,如果没有接口使用 CGLIB
代理。
动态代理和静态代理
优点:
缺点:
功能增强
远程(Remote)代理
防火墙(Firewall)代理
保护(Protect or Access)代理
终于写完了这篇文章,动态代理在我看 AOP
源码时,就感觉挺抽象的
我感觉最大的原因应该在于:代理类动态生成,无法查看,导致对其模糊,从而陷入不理解
但通过这篇文章,我相信,99%
的人应该都可以理解了动态代理模式的来龙去脉
当然,好刀要用在刀刃上,在面试中,若面试官提及 设计模式
、动态代理
、Spring
、dubbo
都可以引出动态代理,基本这篇文章无差别秒杀
如果你能看到这,那博主必须要给你一个大大的鼓励,谢谢你的支持!
喜欢的可以点个关注,后续会更新 Spring
源码系列文章
我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,Java领域新星创作者,喜欢后端架构和中间件源码。
我们下期再见。
来源地址:https://blog.csdn.net/qq_40915439/article/details/129233842
--结束END--
本文标题: 【Spring从成神到升仙系列 一】2023年再不会动态代理,就要被淘汰了
本文链接: https://lsjlt.com/news/372781.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-04-01
2024-04-03
2024-04-03
2024-01-21
2024-01-21
2024-01-21
2024-01-21
2023-12-23
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0