• 彻底理解代理模式


    代理模式

    1. 概念

    • 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

    • 代理模式的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途

    • Java中的代理按照代理类生成时机不同又分为静态代理动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK动态代理cglib 动态代理两种。

    • 优点与缺点

      • 优点:

        • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
        • 代理对象可以扩展目标对象的功能
        • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
      • 缺点:

        • 增加了系统的复杂度;

    2. 结构

    • 代理(Proxy)模式分为三种角色:
      • 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
      • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
      • 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

    3. 静态代理模式

    • 静态代理:在编译时就已经将接口、被代理类、代理类等确定下来。在程序运行之前,代理类的字节码文件就已经生成。

    3.1 案例

    • 客户可以通过房东(被代理类)直接买房子,也可以通过代理来让中间商(代理类)买房子

      • 抽象主题类:卖房子接口

        public interface RentHouse {
            void rentHouse();
        }
        
        • 1
        • 2
        • 3
      • 被代理类:房东

        public class Landlord implements RentHouse{
            @Override
            public void rentHouse() {
                System.out.println("房东卖出房子...");
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
      • 代理类:中间商

        public class Middleman implements RentHouse{
            //通过组合的方式与被代理类产生联系
            private Landlord landlord;
        
            public Middleman(Landlord landlord){
                this.landlord = landlord;
            }
        
            //代理房东去卖房子
            @Override
            public void rentHouse() {
                //调用房东的rentHouse()
                landlord.rentHouse();
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
      • 客户端类

        public class Customer {
            public static void main(String[] args) {
                //创建一个房东
                Landlord landlord = new Landlord();
                
                //创建一个中间商
                Middleman middleman = new Middleman(landlord);
                
                //客户可以通过房东买房子
                System.out.print("直接找房东买房子=>");
                landlord.rentHouse();
                
                System.out.println("======================");
                
                //客户也可以通过中间商买房子
                System.out.print("通过中间商卖买房子=>");
                middleman.rentHouse();
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
      • 输出结果

        .

        日常生活中,我们是很难直接找到房东卖房子的,一般都是通过中间商去买房子,让中间商去代理房东卖房子,这就是代理模式。我们也可以在代理类中添加其他方法,这样可以在不修改被代理类的代码的同时进行扩展。

      • 改进

        public class Middleman implements RentHouse{
            private Landlord landlord;
        
            public Middleman(Landlord landlord){
                this.landlord = landlord;
            }
        
            //代理房东去卖房子
            @Override
            public void rentHouse() {
                seeHouse();
                signContract();
                //调用房东的rentHouse()
                landlord.rentHouse();
            }
        
            public void seeHouse(){
                System.out.println("中间商带顾客看房子");
            }
        
            public void signContract(){
                System.out.println("达成协议=>签合同");
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
      • 输出结果

        在这里插入图片描述

    3.2 小结

    在代理类中定义其他方法,在被代理类的真实方法执行前后,就可以添加其他方法。这种操作,也是使用代理模式的一个很大的优点。最直白的就是在Spring中的面向切面编程(AOP),我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,在代理过程中切入了一些其他操作。

    4. 动态代理模式

    4.1 JDK动态代理

    4.1.1 概述

    • 在静态代理中,代理类是定义好的,在程序运行之前就已经编译完成。而在动态代理中,代理类是在运行时根据代码中的指示动态生成的。与静态代理相比, 动态代理的优势在于可以很方便的对代理类的方法进行统一处理,而不用修改每个代理类中的方法。

    • 在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。

    • InvocationHandler

      • InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable;
        
        • 1
        • 2
        • Object proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
        • Method method:我们所要调用某个对象真实的方法的Method对象
        • Object[] args:指代代理对象方法传递的参数
    • Proxy

      • Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance方法。

        public static Object newProxyInstance(ClassLoader loader,
                                                  Class<?>[] interfaces,
                                                  InvocationHandler h)
                throws IllegalArgumentException{}
        
        • 1
        • 2
        • 3
        • 4
        • loader:一个类加载器对象,定义了由哪个类加载器对生成的代理类进行加载
        • interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口。如果我们提供了这样一个接口对象数组,那生成的代理类也实现了这些接口,代理类就可以调用接口中声明的所有方法。
        • h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

    4.1.2 案例

    • 将上述案例改为动态代理

      • 抽象主题类:卖房子接口

        public interface RentHouse {
            void rentHouse();
        }
        
        • 1
        • 2
        • 3
      • 被代理类:房东

        public class Landlord implements RentHouse{
            @Override
            public void rentHouse() {
                System.out.println("房东卖出房子...");
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
      • 代理类实现接口

        public class MiddleInvocationHandler implements InvocationHandler {
            //invocationHandler持有的被代理对象
            Object target;
        
            public MiddleInvocationHandler(Object target){
                this.target = target;
            }
        
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("执行了=>" + method.getName());
                Object result = method.invoke(target, args);
                return result;
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15

        创建MiddleInvocationHandler类,实现InvocationHandler接口,这个类中组合了一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理,这也是Spring中的AOP实现的主要原理。

      • 客户端类

        public class Customer {
            public static void main(String[] args) {
                //创建一个被代理的对象
                Landlord landlord = new Landlord();
                //创建一个与代理对象相关的InvocationHandler
                MiddleInvocationHandler handler = new MiddleInvocationHandler(landlord);
                //创建一个代理对象MiddleProxy
                RentHouse MiddleProxy = (RentHouse) Proxy.newProxyInstance(
                        Landlord.class.getClassLoader(),
                        Landlord.class.getInterfaces(),
                        handler);
                MiddleProxy.rentHouse();
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 创建了一个需要被代理的房东类Landlord,将landlord对象传给了MiddleInvocationHandler中创建handle实例,调用Proxy类的newProxyInstance创建动态代理对象
        • 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。

    4.1.3 实现原理

    • 上述案例中,我们利用Proxy类的newProxyInstance方法创建了一个动态代理对象,关键源码如下

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JReXXv3t-1659928301278)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220807203209427.png)]

      不难看出 ==Class cl = getProxyClass0(loader, intfs)==这行代码产生了代理类,后面代码中的构造器也是通过这里产生的类来获得,可以看出,这个类的产生就是整个动态代理的关键。代理类是程序在运行过程中动态的在内存中生成的类

    • 我们在命令行对MiddleInvocationHandler.java使用命令javac生成MiddleInvocationHandler.class文件,再通过jd-gui工具对.class文件进行反编译,得到代理类的结构:

      package com.suqu.patterns.proxy.dynamic_proxy;
      
      import ;
      import java.lang.reflect.InvocationHandler;
      import java.lang.reflect.Method;
      import java.lang.reflect.Proxy;
      import java.lang.reflect.UndeclaredThrowableException;
      
      public final class Proxy0 extends Proxy implements RentHouse {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
        
          /**
          * 这是生成代理类的构造方法,传入一个InvocationHandler类型的参数
          */
        public Proxy0(InvocationHandler paramInvocationHandler) {
            /*
            * 调用父类的Proxy构造方法
            * 父类持有protected InvocationHandler h
            * Proxy构造方法:
            * protected Proxy(InvocationHandler h) {
            *         Objects.requireNonNull(h);
            *         this.h = h;
            *     }
            */
          super(paramInvocationHandler);
        }
        
        public final boolean equals(Object paramObject) {...}
        public final String toString() {...}
          
          //这里的rentHouse方法,直接调用了InvocationHandler中的invoke方法并把m3作为参数传递进去
        public final void rentHouse() {
            try {
                  super.h.invoke(this, m3, (Object[])null);
              } catch (RuntimeException | Error var2) {
                  throw var2;
              } catch (Throwable var3) {
                  throw new UndeclaredThrowableException(var3);
            }
        }
        public final int hashCode() {...}
        
          //通过反射获得rentHouse方法,并命名为名m3
        static {
          try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.suqu.patterns.proxy.dynamic_proxy.RentHouse").getMethod("rentHouse", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
          } catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
          } catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
          } 
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60

      解析:在生成代理类的构造方法中,会传入一个InvocationHandler类型的参数,实际上传入的就是我们new的MiddleInvocationHandler,这也就是为什么代理类对象调用的是MiddleInvocationHandler中的invoke()方法,而MiddleInvocationHandler中又有一个被代理类的实例对象landlord,所有最终调用的还是被代理类Landlord中的方法。

    • JDK为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。

    • 我们可以把InvocationHandler看做一个中介类,中介类持有一个被代理对象,在中介类的invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。也就是说,动态代理通过中介类实现了具体的代理功能

    • 总结:生成的代理类:Proxy0 extends Proxy implements RentHouse,我们看到代理类继承了Proxy类,所以也就决定了JDK动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。上面的动态代理的例子,其实就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了处理,对方法耗时统计。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的。

    部分资料参考自该博客代理模式的使用总结

    4.2 CGLIB动态代理

    4.2.1 概述

    • 在上述案例中,如果Landlord并没有实现RentHouse接口,JDK动态代理就无法使用了。因为JDK动态代理要求必须定义接口,对接口进行代理。这个时候就可以使用CGLIB动态代理,他为没有实现接口的类提供代理,为JDK的动态代理提供了补充
    • CGLIB动态代理是一个功能强大,高性能的代码生成包,其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作

    4.2.2 案例

    • CGLIB是第三方提供的jar包,需要导入。

      <dependency>
            <groupId>cglibgroupId>
            <artifactId>cglibartifactId>
            <version>3.3.0version>
      dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 被代理类

      public class Landlord {
          public void rentHouse() {
              System.out.println("房东卖出房子...");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • CglibProxy类,实现了MethodInterceptor

      public class CglibProxy implements MethodInterceptor {
          //重写拦截方法
          @Override
          public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
              //可以在方法执行前添加操作
              System.out.println("cglib动态代理start");
              Object result = methodProxy.invokeSuper(o,objects);
              //可以在方法执行后添加操作
              System.out.println("cglib动态代理end");
              return result;
          }
      
          //获取代理对象的方法
          public Object getCglibProxy(Class<?> clazz){
              //创建动态代理增强类
              Enhancer enhancer = new Enhancer();
              //设置父类,cglib是针对指定的类生成一个子类,所以需要指定父类
              enhancer.setSuperclass(clazz);
              //设置回调函数
              enhancer.setCallback(this);
              //创建代理类
              return enhancer.create();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    • Customer测试类

      public class Customer {
          public static void main(String[] args) {
              Landlord cglibProxy = (Landlord) new CglibProxy().getCglibProxy(Landlord.class);
              cglibProxy.rentHouse();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      CGLIB动态代理会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。在CGLIB底层,其实是借助了ASM这个非常强大的Java字节码生成框架。

    4.2.3 实现过程

    • 实现步骤:

      1. 创建动态代理增强类Enhancer的实例对象
      2. 通过setSuperclass方法来设置目标类。因为生成的代理类要继承被代理类,所有要指定一个父类
      3. 通过setCallback方法来设置拦截对象;
      4. create方法生成目标类的代理类,并返回代理类的实例。
    • Enhancer是CGLIB中非常常用的一个类。和Proxy不同的是,Enhancer既能够代理普通的class,也能够代理接口。Enhancer创建一个被代理对象的子类并且拦截所有的方法调用(包括从Object中继承的toString和hashCode方法)。Enhancer不能够拦截final方法,例如Object.getClass()方法,这是由于Java final方法语义决定的。基于同样的道理,Enhancer也不能对fianl类进行代理操作

    部分参考自CGLIB动态代理

    5. 三种代理模式的比较

    • 静态代理和动态代理

      • 动态代理相较于静态代理,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
      • 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。
    • JDK动态代理和CGLIB动态代理

      • JDK动态代理只能对实现了接口的类生成代理,而不能针对类;CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法

      • JDK动态代理的实现是通过Java的反射技术;CGLIB实现动态代理,底层采用ASM字节码生成框架,使用字节码技术生成代理类

      • 由于底层使用反射,JDK动态代理生成类的过程比较高效;而CGLIB采用ASM字节码框架,相关执行的过程比较高效,生成类的过程可以利用缓存弥补。需要注意的是,由于CGLIB原理是动态生成被代理类的子类,故CGLib不能对声明为final的类或者方法进行代理

        在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理

      • JDK动态代理是不需要第三方库支持,只需要JDK环境就可以进行代理;CGLIB必须依赖于CGLIB的类库。

    6. 使用场景

    • 远程(Remote)代理

      本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。

    • 防火墙(Firewall)代理

      当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。

    • 保护(Protect or Access)代理

      控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

  • 相关阅读:
    京东零售大数据云原生平台化实践
    如何使用程序通过OCR识别解析PDF中的表格
    消息队列_kafka简介
    指定元素懒加载
    成功的性能测试方法的 3 个阶段
    【第五篇】-Maven 构建配置文件
    lvi-sam 总结
    Java核心知识点整理大全6-笔记
    基于FPGA的SD卡的数据读写实现(SD NAND FLASH)
    linux nginx开机自启
  • 原文地址:https://blog.csdn.net/qq_52248567/article/details/126223967