深入浅出设计模式之代理模式(Proxy Pattern)

所谓代理,即控制盒管理对目标对象访问的一个中间层。它代表一个真实的对象,并呈现给外界一个假象–它就是真正的对象,但其实他的一切动作都是调用真实对象来完成的。

一、远程代理

代理模式有很多种,首先我们来讲远程代理。如果我有两个类ClassA和ClassB,它们分别位于不同的机器上(意味着它们在不同的JVM的堆中)。如果ClassA想要调用ClassB中的方法,这时就需要一个远程代理。远程代理就好像是“远程对象的本地代表”,它是一个可以由本地方法调用的对象,其行为会转发到远程对象中。

还是拿在状态模式中的那个糖果机作为例子吧,现在有一个新的需求,想要建立一个糖果监视器来监察糖果机的状态,通常是CEO坐在办公室完成的,所以这就是一个远程调用的应用。这个模型如下图:

在Java中,远程代理是内置的一种模式(RMI),所以我们只需要直接拿出来用就行了。在这之前还是先看看这个模式的具体工作流程吧:

从上面我们可以看到,图中有四个对象,分别是客户对象、客户辅助对象、服务辅助对象和服务对象。辅助对象对象将每个请求转发到远程对象上进行,它使客户就像在调用本地对象方法一样。客户使用客户辅助对象上的方法,仿佛客户辅助对象就是真正的服务。同时对于服务对象来说,调用是本地的,来自服务辅助对象,而不是远程客户。

Java RMI提供了客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI将客户辅助对象称为桩(stub),将服务辅助对象称为骨架(skeleton)。

下面,我们就来完成对Java RMI远程代理的演示。它们分为下面这几个步骤; 1、制作远程接口

1
2
3
public interface MyRemote extends Remote{
    public String sayHello() throws RemoteException;
}

远程接口需要继承自Remote接口,Remote里面其实什么都没有,需要继承自他是为了把当前接口标记为一个远程服务接口,它支持远程调用。 同时,因为每次远程调用都由于网络状况等原因会有一些状况,所以这些东西都必须考虑在内,方法是为接口中的方法都抛出一个RemoteException。

此外,由于远程服务服务中返回的变量都要被打包并通过网络传输,所以要求所有方法的返回值要么是原语(primitive)类型,要么是可序列化的(如果返回的是自己创建的类,那么这个类需要实现Serializable接口)。

2、制作远程实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    protected MyRemoteImpl() throws RemoteException {
    }

    public static void main(String[] args) {
        try {
            MyRemote service = new MyRemoteImpl();
            Naming.rebind("RemoteHello", service);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String sayHello() throws RemoteException {
        return "Server says, 'Hey'";
    }
}

为了要称为远程服务对象,对象需要一些“远程”的功能。最简单的方式就是扩展UnicastRemoteObject,让超类去做这些事。当类被实例化的时候,超类的构造器总是会被调用,如果超类的构造器抛出异常,那么只能声明子类的构造器也抛出异常。

在上面代码的main函数中,有一句Name.rebind,它的作用是用RMI Registry注册服务(即把刚实例化出来的对象放进RMI registry中)这样才能被远程客户调用。其实挡在注册时,真正注册的是stub,因为这才是客户真正需要的。

3、产生stub和skeleton 当我们创建完服务对象后,还需要创建stub和skeleton(即创建客户辅助对象和服务辅助对象)。方法是在远程实现类上执行rmic。rmic是JDK内的一个工具,用来为一个服务类产生stub和skeleton,命名习惯是在服务类后面加上“stub”和“skel”。

首先跳到.class所在的目录(注意目录里不包括包),然后使用rmic xxx命令来生成stub和skeleton:

当执行完上面这段话的时候,就产生了stub

4、启动RMI Registry,提供注册服务。因为启动目录必须访问类的class文件,所以最好是从包含class文件的目录启动(注意目录里不包括包)。

5、启动服务 这里的启动服务指的是创建服务对象,并把它注册到RMI注册表中的过程。启动的地方是在main方法中创建对象并rebind的地方,不一定在实现类中,也可以是另一个独立的启动类中。

6、客户端开始调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyRemoteClient {
    public static void main(String[] args) {
        new MyRemoteClient().go();
    }

    private void go() {
        try {
            MyRemote remote = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
            String s = remote.sayHello();
            System.out.println(s);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果:

1
Server says, 'Hey'

上面go方法中,首先从RMI注册表中找到名字为RemoteHello的远程服务类,其实找到的是一个stub。找到后就调用stub的sayHello。它的结构就像下面这张图:

二、虚拟代理

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

例如我们现在有一个面板,上面需要显示一张图片。如果客户请求,就开始下载,在下载过程中显示"please wait"信息。这本是很简单的一个功能,但问题是用于显示图片的ImageIcon对象和加载图像是同步的,也就是说只有当图片加载完成后构造器才会返回,客户才能继续做其他的事。所以,我们需要一个虚拟对象,它扩展了原对象的功能,他能另开一个线程来下载图片,同时允许客户在这中间做其他事情。

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
public class ImageProxy implements Icon {
    ImageIcon imageIcon;
    URL imageURL;
    Thread retrievalThread;
    boolean retrieving = false;

    public ImageProxy(URL imageURL) {
        this.imageURL = imageURL;
    }

    @Override
    public int getIconWidth() {
        if (imageIcon != null) {
            return imageIcon.getIconWidth();
        }
        return 800;
    }

    @Override
    public int getIconHeight() {
        if (imageIcon != null) {
            return imageIcon.getIconHeight();
        }
        return 600;
    }

    @Override
    public void paintIcon(final Component c, Graphics g, int x, int y) {
        if (imageIcon != null) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("Loading CD cover, please wait", x + 300, y + 190);
            if (!retrieving) {
                retrieving = true;
                retrievalThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        imageIcon = new ImageIcon(imageURL, "CD cover");
                        c.repaint();
                    }
                });
                retrievalThread.start();
            }
        }
    }
}

三、Java API代理

Java在java.lang.reflect包中有自己默认的代理支持,利用这个包可以创建动态代理。所谓动态,其实是指代理类在开始执行代码前是不存在的,他是在代码执行的时候根据需要从传入的接口集创建的(不清楚的话可以继续往下看)。下面是整个Java API代理的通用结构:

其中Proxy类并不需要我们来实现,Java默认会在运行的时候创建,真正需要我们创建的是InvocationHandler类。InvocationHandler的作用是响应代理的任何调用。可以把InvocationHandler看成是代理收到方法调用后,请求做实际工作的对象。

下面我要通过Java API代理来实现一个保护代理。例如在一个社交网络中,我们可以查看别人的资料,但是并不能修改别人的资料,但能对别人的资料或头像点“赞”。同理,我们可以对自己的资料做一切修改,但是就是不能“赞”自己,避免作弊。我们都知道资料会封装在人这个对象中,里面的setter方法全都是公有的,我们要通过保护代理来实现不能对某些setter进行操作,这样就达到了保护的目的。

实际对象接口

1
2
3
4
5
6
7
8
9
10
11
public interface PersonBean {
    public String getName();
    public String getGender();
    public String getInterests();
    public int getHotOrNotRating();

    void setName(String name);
    void setGender(String gender);
    void setInterests(String interests);
    void setHotOrNotRating(int rating);
}

实际实现类

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 PersonBeanImpl implements PersonBean {
    String name;
    String gender;
    String interests;
    int rating;
    int ratingCount = 0;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getInterests() {
        return interests;
    }

    public void setInterests(String interests) {
        this.interests = interests;
    }

    @Override
    public int getHotOrNotRating() {
        if (ratingCount == 0)
            return 0;
        return rating / ratingCount;
    }

    @Override
    public void setHotOrNotRating(int rating) {
        this.rating += rating;
        ratingCount++;
    }

    public int getRatingCount() {
        return ratingCount;
    }

    public void setRatingCount(int ratingCount) {
        this.ratingCount = ratingCount;
    }
}

能修改自己资料的InvocationHandler,但不能“赞”自己

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 {
        if (method.getName().equalsIgnoreCase("setHotOrNotRating")){
            throw new IllegalAccessException("You can't vote for yourself");
        } else {
            return method.invoke(personBean, args);
        }
    }
}

能“赞”别人的InvocationHandler,但是不能修改别人的资料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NoneOwnerInvocationHandler implements InvocationHandler {
    PersonBean personBean;

    public NoneOwnerInvocationHandler(PersonBean personBean) {
        this.personBean = personBean;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equalsIgnoreCase("setHotOrNotRating")) {
            return method.invoke(personBean, args);
        } else {
            throw new IllegalAccessException("can do nothing but set hot or not to others");
        }
    }
}

InvocationHandler里面保留了对实际服务类的引用,当一个请求到达后,会先判断这个请求符合标准不,如果符合,就通过反射来调用实际服务类的方法,如果不符合,就抛出异常。

应用代理

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
public class MatchMakingTestDrive {
    public static void main(String[] args) {
        MatchMakingTestDrive drive = new MatchMakingTestDrive();

        PersonBean joe = new PersonBeanImpl();
        joe.setName("Andy Carroll");

        PersonBean joeProxy = getOwnerProxy(joe);
        try {
            joeProxy.setHotOrNotRating(1);               // 会抛出异常
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(joeProxy.getName());

        PersonBean david = new PersonBeanImpl();
        david.setName("Joe Cole");

        PersonBean davidProxy = getNonOwnerProxy(david);
        davidProxy.setHotOrNotRating(5);
        try {
            davidProxy.setInterests("football");         // 会抛出异常
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static PersonBean getNonOwnerProxy(PersonBean person) {
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new NoneOwnerInvocationHandler(person));
    }

    private static PersonBean getOwnerProxy(PersonBean person) {
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person));
    }
}

这里通过getNoneOwnerProxy和getOwnerProxy来得到两个代理。这其实是使用代理的一般方法,即通过工厂方法返回和实际服务类来自同一个接口的代理,客户神不知鬼不觉地就被骗过了,他并不知道你在下面做了什么手脚。其次,我们使用了Proxy.newProxyInstance来生成真正的代理类,即上图的Proxy类。他是在运行时才创建的,这就是“动态代理”的真谛。