bboyjing's blog

HeadFirst设计模式十三【代理模式】

书上给代理模式的定义是:控制和管理访问。具体是要控制和管理什么,我们本章就来学习下。

场景描述

继续沿用上一章糖果机的例子,假如现在希望能够更好的监控糖果机,要生成一份库存以及机器状态的报告。拷贝上一章最终版的代码,稍做修改即可。

  1. 修改糖果机,添加location信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class GumballMachine {
    ......
    String location;
    public GumballMachine(int numberGumballs, String location) {
    ......
    this.location = location;
    }
    public String getLocation() {
    return location;
    }
    public State getState() {
    return state;
    }
    ......
    }
  2. 添加糖果监视器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class GumballMonitor {
    GumballMachine gumballMachine;
    public GumballMonitor(GumballMachine gumballMachine) {
    this.gumballMachine = gumballMachine;
    }
    public void report() {
    System.out.println("Gumball machine: " + gumballMachine.getLocation());
    System.out.println("Current inventory: " + gumballMachine.getCount() + " gumballs");
    System.out.println("Current state: " + gumballMachine.getState());
    }
    }
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    public class Test {
    public static void main(String[] args) {
    GumballMachine gumballMachine = new GumballMachine(5, "Gumball--1");
    GumballMonitor gumballMonitor = new GumballMonitor(gumballMachine);
    gumballMonitor.report();
    }
    }

上面实现了最简单的监视器,但这还远远不够,而真正的需求是远程监控。目前监视器和糖果机再同一个JVM下运行没,如果想在远程桌面上看到监控信息,该怎么办呢。远程代理(Remote Proxy)可以帮助我们处理这样的场景。

通过远程代理实现远程监控

所谓远程代理,其实就是要创建一个代理,知道如何调用在另一个JVM中的对象的方法。Java已经内置了远程调用功能,其缩写就叫RMI(Remote Method Invocation ),从名字就已经很清楚它的作用了。下面先来了解下RMI,然后再改造糖果机。

RMI

实现RMI需要五个步骤,下面逐一看下:

  1. 制作远程接口,该接口定义出可以让客户远程调用的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 扩展Remote接口,该接口没有方法,具有特别的意义
    */
    public interface MyRemote extends Remote {
    /**
    * 申明抛出RemoteException异常
    * 确保参数、返回值可序列化
    *
    * @return
    * @throws RemoteException
    */
    public String sayHello() throws RemoteException;
    }
  2. 制作远程实现

    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
    /**
    * 实现MyRemote接口
    * 扩展UnicastRemoteObject来称为远程服务对象
    */
    public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    /**
    * 声明默认构造函数,抛出RemoteException,因为超类的构造函数中抛出该异常
    *
    * @throws RemoteException
    */
    public MyRemoteImpl() throws RemoteException {
    }
    @Override
    public String sayHello() throws RemoteException {
    return "Server says, Hey";
    }
    /**
    * 实例化MyRemoteImpl服务,注册到RMI Registry中
    *
    * @param args
    */
    public static void main(String[] args) {
    try {
    MyRemote service = new MyRemoteImpl();
    Naming.rebind("RemoteHello", service);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  3. 产生Stub和Skeleton,Stub是客户辅助对象,Skeleton是服务辅助对象。这两个类是通过JDK提供的一个工具rmic生成的。最新的Java会出现警告,提示不再需要通过rmic生成Stub和Skeleto。

    1
    2
    3
    4
    5
    6
    # 假设标准的maven项目,cd到上面两个类所在的文件夹
    ❯ cd src/main/java/cn/didadu/sample/headFirstPatterns/proxy/rmi
    # 编译java文件
    ❯ javac -d . *.java
    # 使用rmic生成辅助对象
    ❯ rmic -d . cn.didadu.sample.headFirstPatterns.proxy.rmi.MyRemoteImpl
  4. 执行remiregistry,开启一个终端,启动remiregistry

    1
    2
    ❯ cd src/main/java/cn/didadu/sample/headFirstPatterns/proxy/rmi
    ❯ rmiregistry
  5. 启动服务,重新开启一个终端,运行MyRemoteImpl的mian方法

    1
    2
    ❯ cd src/main/java/cn/didadu/sample/headFirstPatterns/proxy/rmi
    ❯ java cn.didadu.sample.headFirstPatterns.proxy.rmi.MyRemoteImpl

服务提供实现完了,下面看下调用方如何实现:

  1. 实现调用逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class MyRemoteClient {
    public static void main(String[] args) {
    try {
    // 通过注册名称到RMI Registry中寻找服务
    MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
    // 调用Stub对象的方法
    String s = service.sayHello();
    System.out.println(s);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  2. 编译、运行,这里要保证所有的class文件都在同一个地方

    1
    2
    3
    4
    ❯ cd src/main/java/cn/didadu/sample/headFirstPatterns/proxy/rmi
    ❯ javac -d . MyRemoteClient.java
    ❯ java cn.didadu.sample.headFirstPatterns.proxy.rmi.MyRemoteClient
    Server says, Hey

至此,我们打下了RMI的基础,下面可以着手来实现糖果机的远程监控了。

通过RMI实现糖果机的远程代理

我们已经有了RMI的基础,现在可以i用RMI实现糖果机的远程代理了。我们来看看糖果机时如何套用RMI框架的:

  1. 拷贝上面场景描述中糖果监视器的全部代码,我们再此基础上进行修改

  2. 创建远程接口

    1
    2
    3
    4
    5
    6
    7
    public interface GumballMachineRemote extends Remote {
    public int getCount() throws RemoteException;
    public String getLocation() throws RemoteException;
    public State getState() throws RemoteException;
    }
  3. State扩展序列化接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * 只要扩展Serializable接口,所有的子类就可以自爱网络上传送了
    */
    public interface State extends Serializable {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
    }
  4. 在每个状态的GumballMachine成员变量前添加transient,因为我们不希望整个糖果机都被序列化并随着State对象一起传送,我们挑一个展示下

    1
    2
    3
    4
    public class WinnerState implements State {
    transient GumballMachine gumballMachine;
    ......
    }
  5. 修改糖果机,将其变成远程接口,并添加启动main函数

    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
    public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote {
    ......
    public GumballMachine(int numberGumballs, String location) throws RemoteException {
    ......
    }
    @Override
    public int getCount() {
    return count;
    }
    @Override
    public String getLocation() {
    return location;
    }
    @Override
    public State getState() {
    return state;
    }
    public static void main(String[] args) {
    try {
    GumballMachine gumballMachine = new GumballMachine(100, "Gumball--1");
    Naming.rebind("GumballMachineRemote", gumballMachine);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  6. 添加客户端调用测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class GumballMachineClient {
    public static void main(String[] args) {
    try {
    GumballMachineRemote gumballMachine = (GumballMachineRemote) Naming.lookup("GumballMachineRemote");
    System.out.println("Gumball machine: " + gumballMachine.getLocation());
    System.out.println("Current inventory: " + gumballMachine.getCount() + " gumballs");
    System.out.println("Current state: " + gumballMachine.getState());
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  7. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 1、进入代码所在的目录,当做工作空间
    ❯ cd src/main/java/cn/didadu/sample/headFirstPatterns/proxy/gumballremote
    # 2、编译java文件
    ❯ javac -d . *.java
    # 3、新开一个终端,在工作空间启动rmiregistry
    ❯ rmiregistry
    # 4、新开一个终端,在工作空间运行GumballMachine
    ❯ java cn.didadu.sample.headFirstPatterns.proxy.gumballremote.GumballMachine
    # 5、新开一个终端,在工作空间运行GumballMachineClient
    ❯ java cn.didadu.sample.headFirstPatterns.proxy.gumballremote.GumballMachineClient
    Gumball machine: Gumball--1
    Current inventory: 100 gumballs
    Current state: cn.didadu.sample.headFirstPatterns.proxy.gumballremote.NoQuarterState@439f5b3d

远程监控版的糖果机实现完了,这个模式给人的感觉是没有什么太多的技巧,基本上就是在使用Java提供的RMI功能。但整个章节并未到此,远程代理只是代理模式的一种,下面看看代理模式还有其他哪些用法。

虚拟代理(Virtual Proxy)

虚拟代理作为创建大的开销对象的代表,经常知道我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。

这里打算建立一个应用程序,用来展示最喜欢的CD封面。我们建立一个CD标题菜单,然后从amazon.com等网站中取得CD封面图。可以创建一个Icon接口从网络上加载图像唯一的问题是,限于连接带宽和网络负载,下载可能需要一些时间,所以在等待图像加载的时候,应该显示一些东西,比如:CD封面加载中,请稍候……。想做到这样,简单的方式就是利用虚拟代理,代理Icon,管理背景的加载,一旦加载完成,代理就把显示的职责委托给Icon。

我们使用ImageProxy类来作为虚拟代理类,其工作流程如下:

  1. ImageProxy首先创建一个ImageIcon,然后开始从网络URL上加载图像
  2. 在加载的过程中,ImageProxy显示CD封面加载中,请稍候……
  3. 当图像加载完毕,ImageProxy把所有方法调用委托给真正的ImageIcon,这些方法包括paintIcon()、getWidth()、getHeight()。
  4. 如果用户请求新的图像,我们就创建新的代理,重复这样的过程

下面来实现代码:

  1. 实现ImageProxy

    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
    public class ImageProxy implements Icon {
    /**
    * 希望在加载后显示出来的真正图像
    */
    ImageIcon imageIcon;
    URL imageURL;
    Thread retrievalThread;
    boolean retrieving = false;
    public ImageProxy(URL imageURL) {
    this.imageURL = imageURL;
    }
    /**
    * 在屏幕上绘制图像
    */
    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
    if (imageIcon != null) {
    // 如果已经有icon,就告诉它画出自己
    imageIcon.paintIcon(c, g, x, y);
    } else {
    // 否则就显示加载中......
    g.drawString("Logading CD cover, please wait...", x + 300, y + 190);
    if (!retrieving) {
    retrieving = true;
    retrievalThread = new Thread(() -> {
    try {
    imageIcon = new ImageIcon(imageURL, "CD Cover");
    c.repaint();
    } catch (Exception e) {
    e.printStackTrace();
    }
    });
    retrievalThread.start();
    }
    }
    }
    @Override
    public int getIconWidth() {
    if (imageIcon != null) {
    return imageIcon.getIconWidth();
    } else {
    return 800;
    }
    }
    @Override
    public int getIconHeight() {
    if (imageIcon != null) {
    return imageIcon.getIconHeight();
    } else {
    return 800;
    }
    }
    }
  1. 创建Image组件,这个是属于Swing范畴,不深究了,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ImageComponent extends JComponent {
    private static final long serialVersionUID = 1L;
    private Icon icon;
    public ImageComponent(Icon icon) {
    this.icon = icon;
    }
    public void setIcon(Icon icon) {
    this.icon = icon;
    }
    @Override
    public void paintComponent(Graphics g) {
    super.paintComponent(g);
    int w = icon.getIconWidth();
    int h = icon.getIconHeight();
    int x = (800 - w) / 2;
    int y = (600 - h) / 2;
    icon.paintIcon(this, g, x, y);
    }
    }
  2. 测试

    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
    public class Test {
    ImageComponent imageComponent;
    JFrame frame = new JFrame("CD Cover Viewer");
    JMenuBar menuBar;
    JMenu menu;
    Hashtable<String, String> cds = new Hashtable<String, String>();
    public Test() throws Exception {
    cds.put("Buddha Bar", "http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
    cds.put("Ima", "http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZZ.jpg");
    cds.put("Karma", "http://images.amazon.com/images/P/B000005DCB.01.LZZZZZZZ.gif");
    cds.put("MCMXC A.D.", "http://images.amazon.com/images/P/B000002URV.01.LZZZZZZZ.jpg");
    cds.put("Northern Exposure", "http://images.amazon.com/images/P/B000003SFN.01.LZZZZZZZ.jpg");
    cds.put("Selected Ambient Works, Vol. 2", "http://images.amazon.com/images/P/B000002MNZ.01.LZZZZZZZ.jpg");
    URL initialURL = new URL((String) cds.get("Selected Ambient Works, Vol. 2"));
    menuBar = new JMenuBar();
    menu = new JMenu("Favorite CDs");
    menuBar.add(menu);
    frame.setJMenuBar(menuBar);
    for (Enumeration<String> e = cds.keys(); e.hasMoreElements(); ) {
    String name = (String) e.nextElement();
    JMenuItem menuItem = new JMenuItem(name);
    menu.add(menuItem);
    menuItem.addActionListener(event -> {
    imageComponent.setIcon(new ImageProxy(getCDUrl(event.getActionCommand())));
    frame.repaint();
    });
    }
    Icon icon = new ImageProxy(initialURL);
    imageComponent = new ImageComponent(icon);
    frame.getContentPane().add(imageComponent);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(800, 600);
    frame.setVisible(true);
    }
    URL getCDUrl(String name) {
    try {
    return new URL((String) cds.get(name));
    } catch (MalformedURLException e) {
    e.printStackTrace();
    return null;
    }
    }
    public static void main(String[] args) throws Exception {
    Test testDrive = new Test();
    }
    }

    这个模式是用ImageProxy代理了ImageIcon,没有什么太花哨的技巧,但是很实用。

保护代理

这一小节我们将利用Java API的动态代理来创建下一个代理实现:保护代理。我们虚构一个约会服务系统,在服务中假如Hot和Not评鉴,Hot表示喜欢对方,Not表示不喜欢。下面就来实现:

  1. 创建一个Person bean,允许设置或取得一个人的信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface PersonBean {
    String getName();
    String getGender();
    String getInterests();
    int getHotOrNotRating();
    void setName(String name);
    void setGender(String gender);
    void setInterests(String interests);
    void setHotOrNotRating(int rating);
    }
  2. 实现Person bean

    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
    public class PersonBeanImpl implements PersonBean {
    String name;
    String gender;
    String interests;
    int rating;
    int ratingCount = 0;
    @Override
    public String getName() {
    return name;
    }
    @Override
    public String getGender() {
    return gender;
    }
    @Override
    public String getInterests() {
    return interests;
    }
    @Override
    public int getHotOrNotRating() {
    if (rating == 0) {
    return 0;
    }
    return rating / ratingCount;
    }
    @Override
    public void setName(String name) {
    this.name = name;
    }
    @Override
    public void setGender(String gender) {
    this.gender = gender;
    }
    @Override
    public void setInterests(String interests) {
    this.interests = interests;
    }
    @Override
    public void setHotOrNotRating(int rating) {
    this.rating += rating;
    ratingCount++;
    }
    }

在这个例子中,我们希望用户可以设置自己的信息,同时又放置他人更改这些信息。而HotOrNot评分则相反,不能更改自己的评分,但是他人可以设置自己的评分。在PersonBean中所有的方法都是公开的,任何用户都可以调用。现在要将这个问题修正。用户不可以改变自己的HotOrNot评分,也不可以改变其他用户的个人信息。需要创建两个代理:一个访问自己的PersonBean对象,另一个访问另一个用户的PersonBean对象。这样,代理就可以控制在每一种情况下允许哪一种请求。下面使用Java API的动态代理来实现:

  1. 创建两个InvocatonHandler,该类实现了代理的行为,是真正跑业务逻辑的地方

    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
    public class OwnerInvocationHandler implements InvocationHandler {
    PersonBean personBean;
    public OwnerInvocationHandler(PersonBean personBean) {
    this.personBean = personBean;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    if (method.getName().startsWith("get")) {
    return method.invoke(personBean, args);
    } else if (method.getName().equals("setHotOrNotRating")) {
    // 禁止调用自己的setHotOrNotRating()方法
    throw new IllegalAccessException();
    } else if (method.getName().startsWith("set")) {
    return method.invoke(personBean, args);
    }
    } catch (InvocationTargetException e) {
    e.printStackTrace();
    }
    return null;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class NonOwnerInvocationHandler implements InvocationHandler {
    PersonBean personBean;
    public NonOwnerInvocationHandler(PersonBean personBean) {
    this.personBean = personBean;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    if (method.getName().startsWith("get")) {
    return method.invoke(personBean, args);
    } else if (method.getName().equals("setHotOrNotRating")) {
    return method.invoke(personBean, args);
    } else if (method.getName().startsWith("set")) {
    // 禁止调用其他set方法
    throw new IllegalAccessException();
    }
    } catch (InvocationTargetException e) {
    e.printStackTrace();
    }
    return null;
    }
    }
  2. 测试

    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
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    public class Test {
    HashMap<String, PersonBean> datingDB = new HashMap<String, PersonBean>();
    public Test() {
    initializeDatabase();
    }
    public static void main(String[] args) {
    Test test = new Test();
    test.drive();
    }
    public void drive() {
    PersonBean joe = getPersonFromDatabase("Joe Javabean");
    PersonBean ownerProxy = getOwnerProxy(joe);
    System.out.println("Name is " + ownerProxy.getName());
    ownerProxy.setInterests("bowling, Go");
    System.out.println("Interests set from owner proxy");
    try {
    ownerProxy.setHotOrNotRating(10);
    } catch (Exception e) {
    System.out.println("Can't set rating from owner proxy");
    }
    System.out.println("Rating is " + ownerProxy.getHotOrNotRating());
    PersonBean noOwnerProxy = getNonOwnerProxy(joe);
    System.out.println("Name is " + noOwnerProxy.getName());
    try {
    noOwnerProxy.setInterests("bowling, Go");
    } catch (Exception e) {
    System.out.println("Can't set interests from non owner proxy");
    }
    noOwnerProxy.setHotOrNotRating(3);
    System.out.println("Rating set from non owner proxy");
    System.out.println("Rating is " + noOwnerProxy.getHotOrNotRating());
    }
    /**
    * 初始化数据
    */
    void initializeDatabase() {
    PersonBean joe = new PersonBeanImpl();
    joe.setName("Joe Javabean");
    joe.setInterests("cars, computers, music");
    joe.setHotOrNotRating(7);
    datingDB.put(joe.getName(), joe);
    PersonBean kelly = new PersonBeanImpl();
    kelly.setName("Kelly Klosure");
    kelly.setInterests("ebay, movies, music");
    kelly.setHotOrNotRating(6);
    datingDB.put(kelly.getName(), kelly);
    }
    PersonBean getPersonFromDatabase(String name) {
    return (PersonBean) datingDB.get(name);
    }
    PersonBean getOwnerProxy(PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance(
    person.getClass().getClassLoader(),
    person.getClass().getInterfaces(),
    new OwnerInvocationHandler(person));
    }
    PersonBean getNonOwnerProxy(PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance(
    person.getClass().getClassLoader(),
    person.getClass().getInterfaces(),
    new NonOwnerInvocationHandler(person)
    );
    }
    }

Java动态代理之前接触的比较多的是在被代理的行为前后插入额外的动作,比如记录日志啊之类的。这一小节看到,在权限控制方便也起到不错的效果,以后写框架的时候可以借鉴。

最后再回顾之前学习的几种容易混淆的设计模式的描述:

  • 装饰者:包装另一个对象,并提供额外的行为
  • 外观:包装许多对象以简化它们的接口
  • 代理:包装另一个对象,并控制对他们的访问
  • 适配器:包装另一个对象,并提供不同的接口