当前位置:网站首页 > 技术博客 > 正文

java什么是代理模式



代理模式( Proxy ),给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。 通常会通过代理对象来为原对象添加额外的功能。
代理模式属于结构型模式主要用于处理类或对象的组合。

上面是比较正式的书面释义,举个通俗点的例子来帮助理解:
周末你躺在床上饿了想吃火鸡面,但是你有点懒,你找你女朋友帮你泡好并端到你面前喂你吃,最后你把火鸡面吃完了,说了句宝宝你真好~
这个例子中:你吃了一包火鸡面,是你做的动作。 但是你请了你的女朋友帮你泡火鸡面,你的女朋友就可以理解为是你的代理,此时你女朋友这个代理帮你泡了火鸡面(代理对象添加了额外的功能)。为了完成你吃火鸡面这件事 ,最后你女朋友还得端到你面前喂你吃(这里的喂你吃火鸡面就相当于 (代理对象控制了原对象的引用) )。

静态代理:
在静态代理中,代理类是在编译时就确定的,即在代码中显式地定义了代理类(需要手动编写一个代理类)。
代理类通常与被代理类实现相同的接口,并且在代理类的方法中调用被代理类的方法。
静态代理的一个缺点是每次添加一个新的功能,都需要创建一个代理类,这样会导致类的数量增加,并且会造成代码的冗余。
代理类通常需要直接引用被代理类,因此代理类对被代理类有一定的依赖关系。如果被代理类的接口发生变化,代理类也需要相应地进行修改,这增加了代码的耦合性。

静态代理实现步骤:

  • ①、定义一个接口用于被代理类实现,定义一个被代理类实现前面定义的接口;
  • ②、自定义一个代理类实现前面定义的接口 ,并重写接口中需要被代理的方法,在重写的方法中调用原方法(被代理类的方法)并自定义一些处理逻辑;
  • ③、通过代理类创建代理对象,使用代理对象替换原对象调用方法;

静态代理图示:
在这里插入图片描述

静态代理的代码示例:

 

运行结果:

 

动态代理:
动态代理是在运行时生成的代理类,而不是在编译时确定的。Java中的动态代理机制主要依靠Java反射机制实现。
动态代理更加灵活,因为它可以在运行时决定要代理的对象及其行为,而不需要显式地为每个类编写代理类。
动态代理通常用于实现横切关注点(cross-cutting concerns AOP相关概念后续在Spring框架相关博客会详细介绍)的功能,
例如日志记录、性能监控、事务管理等。因为动态代理可以在运行时将这些功能动态地添加到方法调用中,而不需要修改原始类的代码。这就达到了解耦的目的。

JDK动态代理实现的步骤:

  • ①、定义一个接口用于被代理类实现,定义一个被代理类实现前面定义的接口;
  • ②、自定义一个类实现 InvocationHandler接口 并重写invoke方法,在 invoke 方法中调用原方法(被代理类的方法)并自定义一些处理逻辑;
  • ③、通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象,使用代理对象调用方法。 参数说明: loader 目标对象的类加载器,interfaces目标对象实现的全部接口,h 自定义的InvocationHandler实例;

JDK动态代理图示:
在这里插入图片描述

JDK动态代理代码示例:

 

运行结果:

 
  • ①、只能代理实现了接口的类: JDK动态代理的机制要求被代理的类必须实现至少一个接口,因为它是基于接口来生成代理类的。这意味着如果目标类没有实现接口,就无法使用JDK动态代理。
  • ②、无法直接代理类的方法: JDK动态代理只能代理接口中定义的方法,无法直接代理类中的方法。如果需要代理类中的方法,就需要使用CGLIB等其他代理机制。
  • ③、性能相对较低: 由于JDK动态代理是基于Java反射机制实现的,相比较于静态代理或者其他代理方式,它的性能会相对较低一些。这是因为在运行时生成代理类和方法调用的过程中,需要进行额外的反射操作,会带来一定的性能开销。

CGLIB(Code Generation Library)是一个Java字节码生成库,它被广泛用于在运行时动态生成新的类以实现代理、混入(Mixin)和其他类似的功能。
CGLIB通过生成目标类的子类,并在子类中重写需要代理的方法来实现代理功能,因此它可以代理那些没有实现接口的类。
CGLIB通常与其他代理机制(如JDK动态代理)相比具有更高的性能,因为它不需要依赖于接口,而且可以代理类中的方法,但是不包括final方法,因为被final修饰的方法无法被子类重写。

Spring框架就集成了CGLIB,默认情况下Spring 的AOP功能实现代理的方式:如果目标对象实现了接口,默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

CGLIB中实现动态代理的关键是 MethodInterceptor 接口和 Enhancer 类。
MethodInterceptor接口中的intercept方法 用来拦截增强被代理类的方法 ,Enhancer类的create方法用来创建代理对象。

CGLIB动态代理的实现步骤:

  • ①、引入CGLIB坐标, 目前最新版本为3.3.0
    https://central.sonatype.com/artifact/cglib/cglib/versions
    在这里插入图片描述
 

选择目标类:选择需要代理的目标类,这个目标类可以是任意的普通类,不一定需要实现接口;

  • ②、实现 MethodInterceptor接口,重写 intercept 方法用于拦截增强被代理类的方法;
  • ③、创建Enhancer对象设置相应参数,调用 create()方法创建代理类;

CGLIB动态代理图示:
在这里插入图片描述

CGLIB动态代理代码示例:

 

①、原理

  • JDK动态代理:
    JDK动态代理的原理主要基于Java的反射机制和java.lang.reflect.Proxy类与java.lang.reflect.InvocationHandler接口。
    当通过代理对象调用方法时,实际上是调用了InvocationHandler的invoke()方法。这个方法内部会通过反射调用实际被代理对象的对应方法,并可以在此前后添加额外的处理逻辑。
    在调用Proxy.newProxyInstance()时,如果代理类还没有被创建,JVM会动态地生成一个实现上述指定接口的代理类的字节码,并加载到JVM中。这个过程是透明的,开发者无需关心具体的生成细节。
  • CGLIB动态代理:
    CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用。
    CGLIB动态代理的核心在于通过继承和字节码操作技术,在运行时动态生成目标类的子类,并在子类中插入自定义的拦截逻辑,以此来达到在不修改目标类源码的情况下增强或控制其行为的目的。

②、使用场景

  • JDK动态代理:
    目标类实现了接口,简单的功能增强
  • CGLIB动态代理:
    无接口的类,复杂的AOP逻辑

③、性能(下面的说法有待考证)

  • JDK动态代理:
    在早期JDK版本中,JDK动态代理的性能通常被认为低于CGLIB,主要是因为每次方法调用都需要通过反射(Method.invoke())来完成,反射操作相对较慢。
    但从JDK 1.8开始,尤其是随着后续版本的不断优化,JDK动态代理的性能有了显著提升。特别是在某些场景下,其性能已经与CGLIB相当,甚至有所超越。
    JDK动态代理在创建代理对象时的开销较小,因为它基于接口实现,不需要生成大量的字节码。
  • CGLIB动态代理:
    CGLIB通过字节码技术生成目标类的子类,这种方式在创建代理实例时的开销较大,因为需要生成新的字节码。
    一旦代理对象创建完成,CGLIB的直接方法调用(通过子类覆盖父类方法)在运行时通常比JDK动态代理的反射调用更快,特别是在频繁调用方法的场景下(这个在JDK8版本的测试下,优势并不明显,也许是方法调用的次数还不够多)。
    CGLIB能够代理没有实现接口的类,提供了更广泛的适用范围,但这也意味着在运行时对类进行了修改,增加了潜在的风险。

简单测试下(使用StopWatch计算,JDK8版本):
分别使用JDK和CGLIB的动态代理去测试创建对象的速度,和调用代理方法的速度,分别进行创建50000次对象的时间比较和使用创建好的代理对象调用50000次代理方法的比较。
PS:被代理类是一样的,只是放在了不同的包下,其中JDK的被代理类多实现了一个接口。

测试代码如下:

 

运行三次结果如下:

 
 

第二个参数就是要传被代理对象实现的接口,这是王八的屁股,规定!

下面就去看看JDK动态代理相关的源码,我们来考证下网上的说法对不对。

进入Proxy.newProxyInstance方法中
有个getProxyClass0方法 (注释:Look up or generate the designated proxy class —— 查找或者生成指定的代理类)
该方法返回生成的代理类的class对象
在这里插入图片描述
再往下看
在这里插入图片描述

再看下getProxyClass0方法内部:(英语真的挺重要,尤其是看源码注释的时候)
在这里插入图片描述
再继续看 ProxyClassFactory
ProxyClassFactory是个静态的内部工厂类
作用就是根据给定的类加载器和接口数组生成、定义并返回代理类
生成的代理类的前缀是$Proxy
在这里插入图片描述
再往下看 代理类包名和类名的生成规则
在这里插入图片描述
答案已经近在眼前了
进入ProxyGenerator.generateProxyClass方法
在这里插入图片描述

那我们就在JVM启动的时候 加上 启动参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

通过IDEA设置JVM启动参数:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个时候再执行下代码 查看结果:

 

结果:

 

可以看到生成的代理类全限定名为: com.kinggm.test.$Proxy0

生成的文件在 项目根目录下:
在这里插入图片描述
由于我的接口

 

并没有加public
所以生成的代理类包名 并不是默认的 com.sun.proxy 而是和我的接口包名相同
并且继承了 Proxy类 和被代理类实现了相同的接口YourBehavior
在这里插入图片描述
最后再看下 JDK自动生成的代理类结构:
(实际上生成的是.class文件,IDEA通过 FernFlower decompiler 插件把 .class字节码文件反编译成我们能看懂的.java类文件)

我们主要看三个地方$Proxy0的构造方法,初始化,重写的eat方法

①、构造方法

 

实际上执行的是父类 Proxy的构造方法,把我们自定义的InvocationHandler 传给父类Proxy中的InvocationHandler h

 
 

③、重写的目标(eat)方法

 

当我们执行代理类的eat方法时实际上执行的是 $Proxy0中的 eat方法

 

$Proxy0中的 eat方法调用的是父类中初始化的 MyInvocationHandler 中的invoke方法

 

所以在执行目标方法eat前后,你女朋友帮你泡火鸡面,带到你面前,你对你女朋友说了 谢谢宝~

分析至此, 发现网上说的好像也有点道理, JDK自动生成的代理类利用继承Proxy来初始化InvocationHandler ,并处理代理对象的生成逻辑,Java又是单继承的语言,想保证代理类和被代理类有一致的行为(方法),就只好让被代理类再实现一个接口,JDK自动生成的代理类也去实现这个接口,就能实现代理类和被代理类有一致的行为(方法)了。最后在代理类中重写被代理类的方法通过调用父类Proxy中初始化的自定义InvocationHandler 中的invoke方法,就能执行到我们自己写的增强逻辑了,同时invoke方法中又去调用了原方法,就完成了整个代理过程喽。

最后再画个图总结下吧:
在这里插入图片描述

  • 上一篇: 开窗函数详解
  • 下一篇: c++右移运算符
  • 版权声明


    相关文章:

  • 开窗函数详解2025-01-25 14:01:01
  • hashset并集2025-01-25 14:01:01
  • oracle数据库expdp命令2025-01-25 14:01:01
  • okgot it2025-01-25 14:01:01
  • sql数据库游标的使用步骤2025-01-25 14:01:01
  • c++右移运算符2025-01-25 14:01:01
  • 服务器性能监控命令2025-01-25 14:01:01
  • stw12025-01-25 14:01:01
  • python课程教学2025-01-25 14:01:01
  • 计数排序优缺点2025-01-25 14:01:01