代理模式应用场景

1、远程代理

远程代理:远程对象的本地代表

  • 远程对象:位于不同地址空间的对象。

  • 本地代表:由本地方法调用的对象,其行为会转发到远程对象中。

    image-20220214185535416

1.1、RMI

本地堆无法获得远程堆的对象,

即变量只能引用本地堆的对象。

示例:变量 machine 无法获得远程堆的对象。

CandyMachine cm1 = <本地堆的对象>;		// ok
CandyMachine machine = <远程堆的对象>;	// no!

远程方法调用(Remote Method Invocation, RMI)

可以找到远程 JVM 的对象,并调用远程方法。

工作原理

涉及结构

  1. 客户堆:

    1. 客户对象:即客户端,需要通过客户辅助对象(代理)间接访问服务对象。
    2. 客户辅助对象(代理):打包调用信息,通过网络向服务辅助对象发出请求。
  2. 服务器堆:

    1. 服务辅助对象:解析客户辅助对象的请求,调用真正的服务对象的方法。
    2. 服务对象:真正提供方法。
  3. 按以上顺序反方向返回请求结果。

    image-20220213165327785

Java RMI API

提供了客户辅助对象(stub)、服务辅助对象(skeleton)

  1. 提供所有运行时的基础设施(如绑定 rebind、查找 lookup)。
  2. 底层使用网络和 I/O,需要处理相应异常。

1.2、实现远程代理

实现步骤

  1. 定义远程接口:作为 stub(代理)和 服务对象(真实主题) 的共同接口。
  2. 创建远程实现:即服务对象。
  3. rmic 产生 Stub 和 Skeleton
  4. 启动 RMI registry
  5. 开启远程服务
  6. 客户获取 Stub 对象,请求执行方法

工作原理

image-20220213185720208

远程接口

继承 Remote 接口(属于标记接口,没有方法)

接口方法的要求

  1. 抛出异常

    1. 客户通过 stub 访问远程,而 stub 底层使用网络 和 I/O。
    2. 相关方法需要抛出 RemoteException 异常。
  2. 变量、返回值:必须是原语或可序列化类型。

    1. 变量和返回值会被打包和解包,通过网络传输,因此要求可序列化。

    2. 可以使用 Java API(如原语类型、字符串等),若是自定义类则需实现 Serializable 接口。

      public interface MyRemote extends Remote {
          String hello() throws RemoteException;
      }
      

远程实现

实现远程接口

  1. 继承 UnicastRemoteObject:用于处理远程服务。

  2. 注册 stub 服务:定义方法用于注册 stub 服务。

    1. 将服务实例化,并使用 RMI Registry 注册;(RMI Registry 需提前运行)

    2. 使用 rebind() 方法注册。

      public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
          protected MyRemoteImpl() throws RemoteException {
          }
      
          @Override
          public String hello() throws RemoteException {
              return "Hello, I'm server!";
          }
      
          public static void main(String[] args) {
              MyRemoteImpl service;
              try {
                  service = new MyRemoteImpl();
                  Naming.rebind("Remote Hello", service);
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      

Stub 和 Skeleton

使用命令行,在远程实现类(即服务对象)中执行 rmic

详细步骤

  1. 切换目录:切换到当前项目的 class 文件生成目录。

    • 若是 IntelliJ IDEA,class 文件在当前项目的 \target\classes 下。
    • 若是 Eclipse,class 文件在当前项目的 \bin 下。
  2. 执行 rmic 命令:注意使用全限类名

    image-20220213175426375

  3. 生成 stub

    image-20220213175654527

  • Java 1.2+:不再需要生成 Skeleton。
  • Java 1.5+:不再需要使用 rmic 生成 Stub,通过动态代理生成。

RMI registry

在当前项目的 class 文件生成目录下,

执行 rmiregistry 命令。

image-20220213175848921

远程服务

创建一个类,使用 rebind() 启动服务。

image-20220213184248845

客户请求

使用 RMI 提供的 lookup() 获取 stub。

  1. 方法参数rmi://IP地址/服务名

  2. 返回值:Remote 接口,需要强转为 MyRemote 类型。

    public class MyRemoteClient {
        public static void main(String[] args) {
            new MyRemoteClient().startClient();
        }
    
        private void startClient() {
            try {
                MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
                System.out.println(service.hello());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

1.3、case:糖果监视器

需求:设计一个监控器(即代理),获取各地糖果机的状态及库存。

1.3.1、准备

CandyMachine 类:糖果机

  • 位置:添加 location 变量,对应构造器参数、getter;

  • 状态:getState() 方法;

  • 库存:getCount() 方法

    image-20220213145843819

State 类:状态

重写 toString() 方法,方便调试。

image-20220213150035276

1.3.2、CandyMonitor

  1. 成员变量:CandyMachine,构造器注入。

  2. report():打印所需信息。

    public class CandyMonitor {
        private CandyMachine machine;
    
        public CandyMonitor(CandyMachine machine) {
            this.machine = machine;
        }
    
        public void report() {
            System.out.println("======= " + machine.getLocation() + " =======");
            System.out.println("Stock:\t\t " + machine.getCount());
            System.out.println("State:\t\t " + machine.getState());
        }
    }
    

1.3.3、远程监视器

分析:当前 CandyMachine 和 CandyMonitor 只能在同一个 JVM 上运行。

通过 RMI 实现远程代理。

image-20220213155659611

远程接口

  1. 继承 Remote 接口

  2. 接口方法抛出 RemoteException 异常。

    public interface MachineRemote extends Remote {
        String getLocation() throws RemoteException;
        State getState() throws RemoteException;
        int getCount() throws RemoteException;
    }
    

State 类

  • 是自定义类,需要实现 Serializable 接口。

  • CandyMachine 实例不需要被序列化,添加 transient 关键字。

    image-20220213203548227

远程实现

  1. 实现远程接口

  2. 继承 UnicastRemoteObject

    image-20220213191502966

Stub 和 Skeleton

切换目录,执行 rmic 命令,生成 Stub 对象。

image-20220213205852034

RMI registry

在当前项目的 class 文件生成目录下,

执行 rmiregistry 命令。

image-20220213205917371

远程服务

新建一个服务启动类,使用 rebind() 用于启动服务。

image-20220213211917162

客户请求

使用 RMI 提供的 lookup() 获取 stub。

image-20220213211936846

2、虚拟代理

2.1、含义

虚拟代理创建开销大的对象的代表。

  • 在对象被成功创建之前,虚拟代理会代替 RealSubject 处理请求。

  • 在对象创建后,虚拟代理将请求委托给 RealSubject。

    image-20220214205808975

2.2、case:图片代理

需求:下载并显示图片。

  1. 使用虚拟代理来代理 ImageIcon 类,管理图片的加载。
  2. 下载未完成时显示提示信息,下载完成后委托 Icon 类显示图片。

类图

设计一个 ImageProxy,代理 ImageIcon 类。

(其中 Icon 和 ImageIcon 都是 java.swing 提供的 API)

image-20220214210816097

实现

实现 Icon 接口,持有真实主题的引用。

  • 成员变量

    • 真实主题:ImageIcon,若值为 null 说明未下载完成。
    • URL:图片 URL,构造器注入
    • retrieve:标记图片是否已被取出。
  • 代理方法:paintIcon() 根据不同场景的操作。

    • 下载完成:调用真实主题的方法,即显示图片。

    • 未下载完成:判断未取出过图片,新建线程并创建真实主题,调用 repaint() 绘制真正的图片。

      public class ImageProxy implements Icon {
          private ImageIcon imageIcon;
      
          private URL imageUrl;
          private boolean retrieve;
      
          public ImageProxy(URL imageUrl) {
              this.imageUrl = imageUrl;
          }
      
          @Override
          public void paintIcon(Component c, Graphics g, int x, int y) {
              if (imageIcon != null) {
                  imageIcon.paintIcon(c, g, x, y);
              }
      
              g.drawString("Loading...", x, y);
      
              if (!retrieve) {
                  retrieve = true;
      
                  Thread retrievalThread = new Thread(() -> {
                      try {
                          imageIcon = new ImageIcon(imageUrl, "Image");
                          c.repaint();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  });
                  retrievalThread.start();
              }
          }
      
          @Override
          public int getIconWidth() {
              return imageIcon != null ? imageIcon.getIconWidth() : 800;
          }
      
          @Override
          public int getIconHeight() {
              return imageIcon != null ? imageIcon.getIconHeight() : 600;
          }
      }
      

3、保护代理

保护代理:基于权限控制对资源的访问,通过动态代理实现。

case:用户评分

需求:模拟一个平台的评分功能。

  • 允许查看任何人的资料和评分;
  • 允许修改个人资料,不允许修改他人资料;
  • 允许对别人评分,不能对自己评分。

结构

  1. Person 接口:name 表示个人资料,like 和 dislike 表示评分。

    public interface Person {
        String getName();
    
        int getLike();
    
        int getDislike();
    
        void setName(String name);
    
        void setLike(int like);
    
        void setDislike(int dislike);
    }
    
  2. 实现类

    public class PersonImpl implements Person {
        private String name;
        private int like;
        private int dislike;
    
        // 实现接口方法
    
        @Override
        public String toString() {
            return name + ":like=" + like + ", dislike=" + dislike;
        }
    }
    

实现保护代理

为 Person 创建动态代理(此处即保护代理)

设计两个 InvocationHandler,限制访问权限。

  1. 拥有者代理:用于访问自己的 Person 对象;
  2. 访问者代理:用于访问其他用户的 Person 对象。

InvocationHandler

拥有者代理

用户要访问自己的 Person 对象时,通过拥有者代理。

  1. 允许调用 getter,查看任意属性。

  2. 允许调用 setter,除了 like 和 unlike。

    public class OwnerInvocationHandler implements InvocationHandler {
        private Person person;
    
        public OwnerInvocationHandler(Person person) {
            this.person = person;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (isAccessible(method.getName())) {
                return method.invoke(person, args);
            } else {
                throw new IllegalAccessException("You can't modify your own likes/dislikes");
            }
        }
    
        private boolean isAccessible(String methodName) {
            return !"setLike".equals(methodName) && !"setDislike".equals(methodName);
        }
    }
    

访问者代理

用户要访问其他用户的 Person 对象时,通过访问者代理。

  1. 允许调用 getter,查看任意属性。

  2. 允许调用 setter,除了 name。

    public class VisitorInvocationHandler implements InvocationHandler {
        private Person person;
    
        public VisitorInvocationHandler(Person person) {
            this.person = person;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (isAccessible(method.getName())) {
                return method.invoke(person, args);
            } else {
                throw new IllegalAccessException("You can't modify other people's name");
            }
        }
    
        private boolean isAccessible(String methodName) {
            return !"setName".equals(methodName);
        }
    }
    

代理工厂

创建一个静态工厂,用于实例化代理对象。

public class PersonProxyFactory {
    public static Person getOwnerProxy(PersonImpl person) {
        return (Person) Proxy
            .newProxyInstance(person.getClass().getClassLoader(),
                              person.getClass().getInterfaces(),
                              new OwnerInvocationHandler(person));
    }

    public static Person getVisitorProxy(PersonImpl person) {
        return (Person) Proxy
            .newProxyInstance(person.getClass().getClassLoader(),
                              person.getClass().getInterfaces(),
                              new VisitorInvocationHandler(person));
    }
}

测试

拥有者

  • 调用 setLike() 和 setDislike() 报错;

  • 其它方法正常调用。

    @Test
    public void testOwnerProxy() {
        PersonImpl person = new PersonImpl();
        person.setName("Jaywee");
    
        Person ownerProxy = PersonProxyFactory.getOwnerProxy(person);
        System.out.println(ownerProxy);
    
        // ownerProxy.setLike(1000);
        // ownerProxy.setDislike(0);
        
        ownerProxy.setName("SecretMrJ");
        System.out.println(ownerProxy);
    }
    

    image-20220216170148511

访问者

  • 调用 setName() 报错;

  • 其它方法正常调用。

    @Test
    public void testVisitorProxy() {
        PersonImpl person = new PersonImpl();
        person.setName("Jaywee");
    
        Person visitorProxy = PersonProxyFactory.getVisitorProxy(person);
        System.out.println(visitorProxy);
    
        // visitorProxy.setName("SecretMrJ");
        visitorProxy.setLike(98);
        visitorProxy.setDislike(2);
        System.out.println(visitorProxy);
    }
    

    image-20220216170247467

posted @ 2022-02-13 19:01  Jaywee  阅读(183)  评论(0编辑  收藏  举报

👇