0%

GoF之代理模式

  • GoF之代理模式
    • 静态代理
    • 动态代理
      • JDK动态代理
      • CGLIB动态代理

十四、GoF之代理模式

14.1 对代理模式的理解

  • 代理模式,就是自己做不了,需要别人来代理,代替自己来完成。

    • 最终这个行为还是要发生,只不过不是由自己来完成,而是由别人代理完成,只是对于客户其他人来说感受不到。
  • 代理模式的作用:

    1. 当一个对象需要受到保护时,可以考虑使用代理模式去完成某个行为。 -> 对象保护
    2. 需要给某个对象的功能进行增强时,可以考虑找一个代理进行增强。 -> 功能增强
    3. A 对象和 B 对象无法直接进行交互时,也可以使用代理模式来解决。
  • 代理模式中的三大角色:

    • 目标对象:需要被代理的对象。

    • 代理对象:代理目标对象的对象。

    • 目标对象和代理对象的公共接口:目标对象和代理对象之间应该具有相同的行为。

      • 为了使用户察觉不到行为是由代理对象完成的,使用户感觉还是由目标对象进行完成的。

      ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/Java-SSM-notebook/img/Spring/代理模式三大角色.png)

  • 使用代理模式,对于客户端程序来说,客户端无法察觉,客户端在使用代理对象的时候,就像在使用目标对象。对于客户端程序来说,使用代理对象时就像在使用目标对象一样。

  • 代理模式是 GoF 23 种设计模式之一,属于结构型设计模式。

  • 代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。

    在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为 “代理” 的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。

  • 代理模式的代码实现有两种形式:

    • 静态代理
    • 动态代理

14.2 静态代理

  • 需求:统计所有业务接口中每一个业务方法的耗时。

    • 解决方案一:硬编码,在每一个业务接口的每一个业务方法中直接添加统计耗时的程序。

      1
      2
      3
      4
      long begin = System.currentTimeMillis();
      // 业务方法代码
      long end = System.currentTimeMillis();
      System.out.println("该业务耗时 " + (end - begin) + " 毫秒");
      • 缺点一:违背 OCP 开闭原则。
      • 缺点二:代码没有得到复用,相同的代码写了很多遍。
    • 解决方案二:编写业务类的子类,让子类继承业务类,对每个业务方法进行重写。

      1
      2
      3
      4
      long begin = System.currentTimeMillis();
      // super.业务方法代码
      long end = System.currentTimeMillis();
      System.out.println("该业务耗时 " + (end - begin) + " 毫秒");
      • 缺点一:虽然解决了 OCP 开闭原则,但是这种方式会导致耦合度很高,因为采用了继承关系。
        • 继承关系是一种耦合度非常高的关系,不建议使用。
      • 缺点二:代码没有得到复用,相同的代码写了很多遍。
    • 解决方案三:代理模式。

      • 优点一:解决了 OCP 问题。
      • 优点二:采用了代理模式,目标对象和代理对象之间是关联关系,可以降低耦合度。
  • 代理模式三大角色:

    • 目标对象和代理对象的公共接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      package com.f.proxy.service;

      /**
      * 订单业务接口
      *
      * @author fzy
      * @date 2024/1/24 19:10
      */
      public interface OrderService { // 代理对象和目标对象的公共接口

      // 生成订单
      void generate();

      // 修改订单
      void modify();

      // 查看订单详情
      void detail();
      }
    • 目标对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.f.proxy.service;

      /**
      * @author fzy
      * @date 2024/1/24 19:12
      */
      public class OrderServiceImpl implements OrderService { // 目标对象
      @Override
      public void generate() { // 目标方法
      System.out.println("订单已生成...");
      }

      @Override
      public void modify() { // 目标方法
      System.out.println("订单已修改...");
      }

      @Override
      public void detail() { // 目标方法
      System.out.println("查看订单详情...");
      }
      }
    • 代理对象

      • 代理对象和目标对象要具有相同的行为,就需要实现同样的接口,使得客户端在使用代理对象的时候,就像在使用目标对象一样。
      • 在代理对象中最终也还是要执行目标对象中的目标方法,为了能够在代理对象中执行目标对象的目标方法,我们可以将目标对象作为代理对象的属性,代理对象中要有目标对象的引用,这种关系为关联关系,耦合度比泛化关系低。
        • 关联关系:在 B 中,有 A 作为其属性,B has a A。
        • 泛化关系:B 继承 A,B is a A。
      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
      package com.f.proxy.service;

      /**
      * @author fzy
      * @date 2024/1/24 19:33
      */
      // 代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口
      public class OrderServiceProxy implements OrderService { // 代理对象
      // 将目标对象作为代理对象的一个属性。这种关系叫做关联关系,比继承关系的耦合度低
      // 代理对象中含有目标对象的引用。关联关系:has a
      // 注意:这里要写公共接口类型,因为公共接口耦合度低
      private OrderService target; // 这就是目标对象,目标对象一定是实现了OrderService接口的

      // 创建代理对象的时候,传目标对象给代理对象
      public OrderServiceProxy(OrderService target) {
      this.target = target;
      }

      @Override
      public void generate() { // 代理方法
      long begin = System.currentTimeMillis();
      // 调用目标对象的目标方法
      target.generate();
      long end = System.currentTimeMillis();
      System.out.println("该业务耗时 " + (end - begin) + " 毫秒");
      }

      @Override
      public void modify() { // 代理方法
      long begin = System.currentTimeMillis();
      // 调用目标对象的目标方法
      target.modify();
      long end = System.currentTimeMillis();
      System.out.println("该业务耗时 " + (end - begin) + " 毫秒");
      }

      @Override
      public void detail() { // 代理方法
      long begin = System.currentTimeMillis();
      // 调用目标对象的目标方法
      target.detail();
      long end = System.currentTimeMillis();
      System.out.println("该业务耗时 " + (end - begin) + " 毫秒");
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.f.proxy.client;

      import com.f.proxy.service.OrderService;
      import com.f.proxy.service.OrderServiceImpl;
      import com.f.proxy.service.OrderServiceProxy;

      /**
      * @author fzy
      * @date 2024/1/24 19:15
      */
      public class Test {
      public static void main(String[] args) {
      // 创建目标对象
      OrderService target = new OrderServiceImpl();
      // 创建代理对象
      OrderService proxy = new OrderServiceProxy(target);
      // 调用代理对象的代理方法
      proxy.generate();
      proxy.modify();
      proxy.detail();
      }
      }
    • 使用静态代理模式,没有修改原先写好的类,符合 OCP 原则,且在代理类中只是有目标对象的引用,耦合度比前两种方法更低。

  • 目前我们使用的是静态代理,这个静态代理的缺点是什么?

    • 类爆炸。假设系统中有 1000 个接口,那么每个接口都需要对应代理类,这样类的数量会急剧膨胀,不好维护。
  • 怎么解决类爆炸问题?

    • 可以使用动态代理来解决这个问题。
  • 我们不会在实际中去使用静态代理,这里只是为了帮助我们过渡到动态代理。

14.3 ★动态代理

  • 在内存中动态的生成字节码代理类的技术,叫做:动态代理。
  • 动态代理:在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量,解决代码复用的问题。
  • 在内存当中动态生成类的技术常见的包括:
    • JDK 动态代理技术:java.lang.reflect.Proxy,只能代理接口,即只适合有一个目标类和一个公共接口的情况。
    • CGLIB 动态代理技术:CGLIB(Code Generation Library) 是一个开源项目,它是一个强大的,高性能,高质量的 Code 生成类库,它可以在运行期扩展 Java 类与实现 Java 接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比 JDK 动态代理要好(底层有一个小而快的字节码处理框架ASM)。
      • 由于动态生成的类是在内存中生成的,所以可以采用继承的方式,无所谓其耦合度。
    • Javassist 动态代理技术:Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库,是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态 “AOP” 框架。

14.3.1 JDK动态代理

  • JDK 动态代理技术:java.lang.reflect.Proxy,只能代理接口,即只适合有一个目标类和一个公共接口的情况。

  • 代理模式三大角色:

    • 目标对象和代理对象的公共接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      package com.f.proxy.service;

      /**
      * 订单业务接口
      *
      * @author fzy
      * @date 2024/1/24 19:10
      */
      public interface OrderService { // 代理对象和目标对象的公共接口

      // 生成订单
      void generate();

      // 修改订单
      void modify();

      // 查看订单详情
      void detail();
      }
    • 目标对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.f.proxy.service;

      /**
      * @author fzy
      * @date 2024/1/24 19:12
      */
      public class OrderServiceImpl implements OrderService { // 目标对象
      @Override
      public void generate() { // 目标方法
      System.out.println("订单已生成...");
      }

      @Override
      public void modify() { // 目标方法
      System.out.println("订单已修改...");
      }

      @Override
      public void detail() { // 目标方法
      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
      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
      package com.f.proxy.client;

      import com.f.proxy.service.OrderService;
      import com.f.proxy.service.OrderServiceImpl;
      import com.f.proxy.service.TimeInvocationHandler;

      import java.lang.reflect.Proxy;

      /**
      * 客户端程序
      *
      * @author fzy
      * @date 2024/1/24 20:19
      */
      public class Client {
      public static void main(String[] args) {
      // 创建目标对象
      OrderService target = new OrderServiceImpl();
      // 创建代理对象(使用JDK动态代理)
      /**
      * 1. newProxyInstance 翻译为:新建代理对象
      * 通过调用这个方法可以创建代理对象
      * 本质上,这个Proxy.newProxyInstance方法的执行做了两件事:
      * 第一件事:在内存中动态生成了一个代理类的字节码class
      * 第二件事:通过内存中生成的代理类,实例化了代理对象并返回
      * 2. 关于newProxyInstance方法的三个重要参数,每一个什么含义,有什么用?
      * 第一个参数 ClassLoader loader
      * 类加载器。
      * 在内存中生成的字节码也是class文件,要执行也得先加载到内存当中。
      * 加载类就需要使用类加载器,所以这里需要指定类加载器。
      * 并且JDK要求,代理类的类加载器必须和目标类的类加载器使用同一个。
      * 第二个参数 Class<?>[] interfaces
      * 代理类和目标类实现的同一个或同一些接口。
      * 第三个参数 InvocationHandler h
      * 调用处理器。
      * 在调用处理器接口中,编写的就是增强代码。
      * 因为具体要添加什么增强代码,需要我们自己写。
      * 既然是接口,就要写接口的实现类
      * 注意:代理对象和目标对象实现的接口一样,所以可以向下转型。
      */
      //Object obj = Proxy.newProxyInstance(类加载器, 代理类要实现的接口, 调用处理器);
      OrderService proxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
      target.getClass().getInterfaces(),
      new TimeInvocationHandler(target));
      // 调用代理对象的代理方法
      proxy.generate();
      proxy.modify();
      proxy.detail();
      }
      }
      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
      package com.f.proxy.service;

      import java.lang.reflect.InvocationHandler;
      import java.lang.reflect.Method;

      /**
      * 专门负责计时的调用处理器对象
      * 在这个调用处理器中,负责编写计时相关的增强代码。
      *
      * @author fzy
      * @date 2024/1/24 20:36
      */
      public class TimeInvocationHandler implements InvocationHandler {
      // 目标对象
      private Object target;

      public TimeInvocationHandler(Object target) {
      // 将目标对象赋值给成员变量
      this.target = target;
      }

      /**
      * 1. 为什么强行要求你必须实现InvocationHandler接口?
      * 因为一个类实现接口就必须实现接口中的方法。
      * 以下这个方法的方法名为invoke,因为JDK在底层调用invoke方法的程序已经提前写好了
      * 注意:invoke方法不是我们程序员负责调用的,JDK负责调用。
      * 2. invoke方法什么时候被调用?
      * 当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器中的invoke方法被调用
      * 3. invoke方法的三个参数:
      * invoke方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数。
      * 我们可以在invoke的方法体中直接使用这三个参数。
      * 第一个参数:Object proxy
      * 代理对象的引用。
      * 这个参数使用较少
      * 第二个参数:Method method
      * 目标对象的目标方法(要执行的目标方法)
      * 第三个参数:Object[] args
      * 目标方法的实参
      * invoke方法在执行过程中,使用method来调用目标对象的目标方法
      */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      long begin = System.currentTimeMillis();
      // 注意:调用代理对象的代理方法的时候,目标对象的目标方法得保证执行。
      Object retValue = method.invoke(target, args);
      long end = System.currentTimeMillis();
      System.out.println("该业务耗时 " + (end - begin) + " 毫秒");
      // 如果代理对象调用代理方法之后,需要返回结果的话,invoke方法必须将目标对象的目标方法的执行结果继续返回
      return retValue;
      }
      }
Proxy.newProxyInstance
  • 通过该方法创建代理对象。
    • 本质上,这个 Proxy.newProxyInstance 方法的执行做了两件事:
      • 第一件事:在内存中动态生成了一个代理类的字节码 class
      • 第二件事:通过内存中生成的代理类,实例化了代理对象并返回。
  • Proxy.newProxyInstance 方法的三个参数:
    • **类加载器 ClassLoader loader**:内存中动态生成的类的字节码需要加载到 JVM 中,需要类加载器,JDK 要求代理类的类加载器和目标类的类加载器要是同一个。
    • **代理类要实现的接口 Class<?>[] interfaces**:代理类要和目标类实现相同的接口,代理类需要实现的接口需要我们进行告知。
    • **调用处理器 InvocationHandler h**:JDK 不可能知道我们要代理类增强目标类哪些功能,JDK 不知道增强的代码,这个需要我们进行编写,我们可以通过调用处理器告诉 JDK 代理类的增强代码,调用处理器 InvocationHandler 是一个接口,需要我们进行实现,在实现方法中编写增强代码。
InvocationHandler
  • 实现调用处理器接口 InvocationHandler,需要实现调用处理器的 invoke 方法,实现该方法其实就是在实现代理类实现公共接口时需要实现公共接口的方法的方法体。增强代码就在调用处理器接口 InvocationHandlerinvoke 方法中编写
  • invoke 方法不是我们程序员负责调用的,是由 JDK 在底层进行调用,如何调用 invoke 方法,JDK在底层已经写好了,当代理对象调用代理方法的时候, invoke 方法会被调用
    • 注意:调用代理对象的代理方法的时候,目标对象的目标方法得保证执行。也就是说,需要在 invoke 方法中执行目标对象的目标方法。
  • invoke 方法的三个参数:
    • **代理对象 Object proxy**:代理对象的引用,使用较少。
    • **目标对象的目标方法 Method method**:要执行的目标方法。
    • **目标方法的实参 Object[] args**:传给目标方法的参数。
  • invoke 方法的返回值:
    • 如果代理对象调用代理方法之后,需要返回结果的话,invoke 方法必须将目标对象的目标方法的执行结果继续返回。

14.3.2 CGLIB动态代理

  • CGLIB 动态代理可以代理接口,也可以代理类。

    • CGLIB 动态代理底层采用继承的方式实现,所以被代理的目标类不能使用 final 修饰。
---------------The End---------------