【从0开始学设计模式-12| 代理模式】

1.概念

代理模式(Proxy Pattern)的定义如下:

代理,以其最一般的形式,是一个类,用作其他东西的接口。代理是一个包装器或代理对象,客户端正在调用它来访问幕后的真实服务对象。代理的使用可以简单地转发到真实对象,或者可以提供额外的逻辑。在代理中,可以提供额外的功能,例如,当对真实对象的操作是资源密集型时缓存,或者在调用对 真实对象的操作之前检查前提条件。

控制对于某个对象的访问必要性

比如有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。

  • 使用延迟加载 :可以使用延迟加载的方式来解决,当需要用到的时候再初始化对象。
    • 如果这个对象的初始化过程非常麻烦,在不同的客户端都要去做这个初始化,操作非常不方便还会出现很多重复代码。
    • 理想状态将控制对象的代码植入到原始对象,但是如原始对象是第三方库就无能为力了。
  • 使用代理对象:新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。

!tip

总结一下:为了节省资源,我们想到了延迟加载的方式,但是在具体技术落地的时候会遇到问题:

  • 巨型对象初始化过程复杂,各个模块都搞延迟加载,会导致到处都是重复且丑陋的代码
  • 把这套逻辑写到原巨型对象,由于原巨型对象是属于第三方库,无法改变源码

因此我们引入了"代理对象",就像一个智能中间人,伪装成原对象的样子,帮助我们进行了复杂的操作

2.代理模式的结构

代理模式包含如下三个角色:

  • Service Interface (服务接口)声明了服务接口(真实角色和代理角色共有接口)。 这样一来在任何使用真实角色的地方都可以使用代理角色,客户端通常需要针对服务接口进行编程。
  • Service(服务) :它定义了代理角色所代表的真实对象,在真实角色中实现了真实的业务操作,客 户端可以通过代理角色间接调用真实角色中定义的操作。
  • Proxy (代理)包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象 的整个生命周期进行管理。
  • Client(客户端):能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码 中使用代理。

与其他模式的对比

  • 适配器模式 能为被封装对象提供不同的接口, 代理模式 能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。
  • 外观模式 与代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
  • 装饰模式 和代理模式有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期 , 而装饰的生成则总是由客户端进行控制

3.代理模式的实现

3.1 类图设计

!tip

这个例子是这样的:使用代理对象,保证只能最多三个人进入象牙塔

3.2 代码实现

  • 服务接口

    java 复制代码
    public interface WizardTower {
      void enter(Wizard wizard);
    }
    java 复制代码
    @Data
    @AllArgsConstructor
    public class Wizard {
      private String name;
    }
  • 服务对象

    java 复制代码
    public class LvoryTower implements WizardTower {
      @Override
      public void enter(Wizard wizard) {
        System.out.println(wizard.getName() + "进入了象牙塔");
      }
    }
  • 代理对象

    java 复制代码
    public class WizardTowerProxy implements WizardTower {
      private static final int NUM_WIZARDS_ALLOWED = 3;
      private final WizardTower wizardTower;
      private int currNumber;
    
      public WizardTowerProxy(WizardTower wizardTower) {
        this.wizardTower = wizardTower;
      }
    
      private boolean checkAccess() {
        return currNumber < WizardTowerProxy.NUM_WIZARDS_ALLOWED;
      }
    	//增强逻辑
      @Override
      public void enter(Wizard wizard) {
        if (checkAccess()) {
          wizardTower.enter(wizard);
          currNumber++;
        } else {
          System.out.println(wizard.getName() + ",你不能在进入");
        }
      }
    }
  • 客户端

    java 复制代码
    public class App {
      public static void main(String[] args) {
        WizardTowerProxy proxy = new WizardTowerProxy(new LvoryTower());
        proxy.enter(new Wizard("阿伟"));
        proxy.enter(new Wizard("图灵机"));
        proxy.enter(new Wizard("云海"));
        proxy.enter(new Wizard("东东哥"));
      }
    }

4.远程代理

远程代理(Remote Proxy)是一种常用的代理模式,它使得客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速响应并处理客户端的请求。

远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远 程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方 法的调用。

远程代理又称为大使(Ambassador)

4.1类图设计

下面来模拟一下,网络请求。

!tip

这个例子是模拟网络请求,服务对象模拟网络不稳定的第三方接口。代理对象模拟重试次数。

4.2 代码实现

  • 服务接口

    java 复制代码
    public interface RemoteServiceInterface {
      long doRemoteFun(int val);
    }
  • 服务对象

    java 复制代码
    public class RemoteService implements RemoteServiceInterface {
      private Random random;
      //单例模式,保证全局只有一个真实服务
      private static RemoteService service = new RemoteService();
    
      private RemoteService() {
        random = new Random();
      }
    
      public static RemoteService getService() {
        return service;
      }
    
      @SneakyThrows
      @Override
      public long doRemoteFun(int val) {
        //使用 Random 模拟执行时间(0~1000毫秒)
        //如果耗时超过 500ms,它就模拟宕机或超时
        long waitTime = random.nextInt(1000);
        Thread.sleep(waitTime);
        return waitTime > 500 ? -1 : val * 10;
      }
    }
  • 代理对象

    java 复制代码
    public class ServiceAmbassador implements RemoteServiceInterface {
      private final int RETRIES = 3;
      private final long DELAY_MS = 1000;
    	//调用 RemoteService 的前后打上时间戳,计算并打印出执行耗时
      private long checkLatency(int val) {
        long start = System.currentTimeMillis();
        long res = RemoteService.getService().doRemoteFun(val);
        long dlt = System.currentTimeMillis() - start;
        System.out.println("执行消耗时间" + dlt + "ms");
        return res;
      }
    
      @SneakyThrows
      @Override
      public long doRemoteFun(int val) {
        long res = -1;
        //for循环,最多重试3次
        for (int i = 1; i <= RETRIES; i++) {
          res = checkLatency(val);
          if (res != -1) {
            break;
          } else {
            System.out.println("请求重试第(" + i + ")");
            Thread.sleep(DELAY_MS);
          }
        }
        return res;
      }
    }
  • 客户端

    java 复制代码
    public class Client {
      private ServiceAmbassador serviceAmbassador = new ServiceAmbassador();
    
      public long useService(int val) {
        long res = serviceAmbassador.doRemoteFun(val);
        if (res == -1) {
          System.out.println("请求失败:" + res);
        } else {
          System.out.println("执行结果:" + res);
        }
        return res;
      }
    
      public static void main(String[] args) {
        Client client1 = new Client();
        Client client2 = new Client();
        client1.useService(13);
        client2.useService(14);
      }
    }

5虚拟代理

虚拟代理 (Virtual Proxy)也是一种常见的代理模式,对于一些占用系统资源较多的或者加载时间长的对象,可以给系统提供一个虚拟代理。

  • 如果加载时间过长,可以在虚拟代理对象中使用多线程执行异步加载;

  • 如果系统资源过多,可以在虚拟代理对象中执行延迟加载,使用完成后也可以做资源释放。

5.1 类图设计

!tip

这个案例是使用代理对象模拟大图片的异步延迟加载,当网络慢时,先渲染一个默认图标,等高清大图下载好了再替换上去

5.2 代码实现

  • 服务接口

    java 复制代码
    public interface Icon {
      int getWidth();
      int getHeight();
      void paintIcon(Container container);
    }
    java 复制代码
    public class Container {
      void showContent(int w, int h, String content) {
        System.out.println(content);
        for (int i = 0; i < h; i++) {
          for (int j = 0; j < w; j++) {
            System.out.print("*");
          }
          System.out.println();
        }
      }
    }
  • 服务对象

    java 复制代码
    public class ImageIcon implements Icon {
        @Override
        public int getWidth() {
            return 10;
        }
    
        @Override
        public int getHeight() {
            return 10;
        }
    
        @Override
        public void paintIcon(Container container) {
            container.showContent(getWidth(), getHeight(), "真实图片");
        }
    }
  • 代理对象

    java 复制代码
    public class ImageProxy implements Icon {
      private ImageIcon icon;
      
      @Override
      public int getWidth() {
        if (icon != null) {
          return icon.getWidth();
        }
        return 1;
      }
    
      @Override
      public int getHeight() {
        if (icon != null) {
          return icon.getHeight();
        }
        return 1;
      }
    
      @Override
      public void paintIcon(Container container) {
        if (icon != anull) {
          icon.paintIcon(container);
        } else {
          //默认渲染:先渲染一个默认图标
          container.showContent(getWidth(), getHeight(), "默认图标");
          new Thread(()->{
            //模拟图片加载
            icon = new ImageIcon();
            try {
              Thread.sleep(3000);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            //刷新显示
            icon.paintIcon(container);
          }).start();
        }
      }
    }
  • 客户端

    java 复制代码
    public class App {
      public static void main(String[] args) {
        //创建容器
        Container container = new Container();
        //创建代理对象
        Icon icon = new ImageProxy();
        //显示图片
        icon.paintIcon(container);
      }
    }

6.Java动态代理

在传统代理模式中,要求代理类和真实类是预先定义好的,这些类子啊编译后都会生成对应的class文件, 这种方式称为静态代理(Static Proxy)

**动态代理(Dynamic Proxy)**可以让系统在运行时,根据具体实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实类而且可以代理不同的方法,它在事务管理、AOP等领域中发挥了重要作用。

JDK1.3开始就提供了对动态代理的支持,使用Java语言使用动态代理需要用到一些反射包下面的类。

Proxy类

  • Proxy类提供了用于手动创建动态代理类和实例对象的方法,它是所创建代理类的父类。
  • 常用方法如下:
    • getProxyClass :用于返回一个Class类型的代理类。
      • 在参数中需要提供类加载器和指定的代理接口数组(与真实类的接口一致)。
    • newProxyInstance :用于返回一个动态代理类实例,
      • 第一个参数表示代理类的类加载器;
      • 第二个参数表示代理类所实现的接口列表;
      • 第三个参数表示指派的调用处理程序类。

InvocationHandler接口

代理程序处理接口,该接口作为代理实例的调用处理者的公共父类。

该接口声明的方法:

  • invoke:该方法用于处理对代理类实例的方法调用并返回对应结果,当一个代理实例的方法被调用 的时候将自动调用该方法。
    • 第一个参数表示代理类的实例;
    • 第二个参数表示需要代理的方法;
    • 第三个参数表示代理方法的参数数组。

动态代理类需要在运行时指定被代理的真实类的接口,客户端在调用动态代理对象的方法时调用请求会将请求自动转发给InvocationHandler对象的invoke方法,由invoke方法来实现对请求的统一处理。

6.1 类图设计

!tip

这个例子是,利用JDK动态代理,进行日志的记录。

  • 解决的问题:如果是静态代理,如果你的系统里有 100 个 DAO(数据库访问对象)都需要加日志记录,你就得手写 100 个代理类。JDK动态代理利用 Java 的反射机制 ,在程序运行期间动态生成代理对象。

6.2代码实现

  • 服务接口

    java 复制代码
    public interface AbstractUserDAO {
      boolean updateById(int id);
    }
    java 复制代码
    public interface AbstractDocumentDAO {
      boolean deleteById(int id);
    }
  • 服务对象

    java 复制代码
    public class UserDAO implements AbstractUserDAO {
      @Override
      public boolean updateById(int id) {
        return id == 7683;
      }
    }
    java 复制代码
    public class DocumentDAO implements AbstractDocumentDAO {
      @Override
      public boolean deleteById(int id) {
        return id == 996;
      }
    }
  • 代理对象

    java 复制代码
    public class LogHandler implements InvocationHandler {
      
      // 内部持有的真实服务对象
      private final Object proxyObj;
    
      public LogHandler(Object proxyObj) {
        this.proxyObj = proxyObj;
      }
    
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //前置增强:在目标方法执行前,插入横向逻辑(打印操作日志)
        beforeInvoke(args[0]);
        //转发调用
        //利用反射,让传入的真实业务对象(proxyObj)去执行当前的这个方法(method),并传入参数(args)
        Object res = method.invoke(proxyObj, args);
        //后置增强:在目标方法执行完毕并拿到结果后,插入横向逻辑(打印结果日志)
        afterInvoke(res, args[0]);
        return res;
      }
    
      private String getLogPrefixStr() {
        int h = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
        int m = Calendar.getInstance().get(Calendar.MINUTE);
        int s = Calendar.getInstance().get(Calendar.SECOND);
        return h + ":" + m + ":" + s + " " + proxyObj.getClass().getSimpleName() + " ";
      }
    
      private void beforeInvoke(Object id) {
        System.out.println(getLogPrefixStr() + "你正在对id为" + id + "的数据进行操作");
      }
    
      private void afterInvoke(Object res, Object id) {
        System.out.println(getLogPrefixStr() + "对id为" + id + "的数据操作结果为" + res);
      }
    }
  • 客户端

    java 复制代码
    public class Client {
      public static void main(String[] args) {
        //创建代理对象代理UserDAO
        AbstractUserDAO ud = new UserDAO();
        InvocationHandler logHandler = new LogHandler(ud);//注入真实的服务对象
        AbstractUserDAO udProxy = (AbstractUserDAO) Proxy.newProxyInstance(
          AbstractUserDAO.class.getClassLoader(),
          new Class[]{AbstractUserDAO.class},
          logHandler);
        udProxy.updateById(1920);
        //创建代理对象代理DocumentDAO
        AbstractDocumentDAO dd = new DocumentDAO();
        logHandler = new LogHandler(dd);
        AbstractDocumentDAO ddProxy = (AbstractDocumentDAO) Proxy.newProxyInstance(AbstractDocumentDAO.class.getClassLoader(),new Class[]{AbstractDocumentDAO.class},logHandler);
        ddProxy.deleteById(996);
      }
    }

通过使用动态代理可以实现对多个真实类的统一代理和集中控制。

JDK提供的动态代理只能代理一个或多个接口

如果需要动态代理具体类或抽象类,可以使用CGLib工具类。

7.其他代理模式

  • 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享 这些结果。
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。

可以自行查阅资料了解

8.代理模式的适用场景

代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进 行控制。代理模式类型较多,其中远程代理、虚拟代理、保护代理等在软件开发中应用非常广泛。在Java RMI、EJB、WEB Service、Spring AOP等技术和框架中都使用了代理模式。

8.1 主要优点

代理模式的共同优点如下:

  • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • 客户端可以针对抽象角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有 较好的灵活性和可扩展性。

此外,不同类型的代理模式也具有独特的优点,例如:

  • 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对 象和操作移至性能更好的计算机上,提高系统的整体运行效率。
  • 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系 统的运行开销。
  • 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优 化系统性能,缩短执行时间。
  • 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。

8.2 主要缺点

  • 由于在客户端和真实类之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
  • 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。

8.3 适用环境

代理模式的类型较多,不同类型的代理模式有不同的优缺点,它们应用于不同的场合:

  • 当客户端对象需要访问远程主机中的对象时可以使用远程代理
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行 时间时可以使用虚拟代理
  • 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果 时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需 直接从临时缓冲区获取操作结果即可。
  • 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理
  • 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理

如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!

相关推荐
触底反弹2 小时前
🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚!
人工智能·设计模式·面试
杨充1 天前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 天前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 天前
6.设计原则的全景图
设计模式·开源·全栈
杨充1 天前
2.面向对象的特性
设计模式
杨充1 天前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 天前
8.反模式与坏味道
设计模式·开源·代码规范
杨充1 天前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法