《Head First 设计模式》读书笔记(11)—— 代理模式 Proxy

本文共9000词,阅读需要30分钟,本文相关代码地址被托管在 gitee

代理就是要进行控制和管理访问,代理有很多种,这一章很精彩

代理模式——接着状态模式的糖果机说

背景:糖果机老板希望可以在看到这个糖果机的运行报告以及库存信息

当然最直接的想法就是新建一个GumballMonitor类,里面实现个report方法,然后一被使用就调用这个方法,差不多就这个意思,基本上就完美解决了

新背景:希望能够远程监控,不能只是在一个jvm里才能监控

这些就是远程代理。
梳理一下现在的情况,现在虽然可以监控,但是必须在同一个JVM里,所以我们要使用远程代理模式,具体往下看
远程代理就好比“远程对象的本地代表”。
什么是远程对象,就是一种活在不同的jvm堆里,在不同的地址空间运行的远程对象。
什么是本地代表,这是一种可以有本地方法调用的对象,其行为会在转发到远程对象中。

本例的代理模式具体流程大概就是:
糖果机就是远程对象,它真的在卖东西,在跟买的人交互
而代理就是假装自己是糖果机,和监视器去交互
监视器就是客户,以为自己在实时监控远程的糖果机,实际上只是在和代理沟通,使用的就是本地代表。

关于Java RMI(Java Remote Method Invocation)

这本书也讲了,先学下,如果没搞明白,估计要单独系统看一下这个东西
直接翻译就是 远程方法调用。
先照本宣科一下:
上面说的很多,但是怎么去调用远程的一个类呢?你不可能直接Duck d=new “别人电脑上的一个子鸭子类”
变量d只能引用当前代码语句的同一堆空间的对象,这时候就需要java RMI,帮助你实现远程JVM的对象,并允许我们调用他的方法

远程方法101

如果我们还不知道RMI,但是我们现在就是要实现远程方法的调用。我们可以这样做:
我们就是客户端,远程就是服务端,中间就是一些辅助对象(还没往下看,但我估计是利用Tcp的Socket通信),
客户使用辅助对象上的方法,仿佛这些辅助对象就是真正的服务,辅助对象帮助我们和服务端机交互。

果然就是Socket通信,但可能不是基础的消息发送过去:

服务器通过Socket连接从客户辅助对象接收请求,将调用的信息解包,然后服务器亲自调用真正的方法。所以服务器连接的服务辅助对象;
而服务辅助对象从服务器中得到返回值,将它打包然后通过Socket的输出流,客户辅助对象解包然后将返回值交给客户对象。

客户对象--客户辅助对象--服务辅助对象--服务对象

RMI提供客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI直接实现了这些,让你不用亲自编写网络I/O的代码,客户调用远程方法就和在运行在客户自己的本地JVM上对对象进行正常方法调用一样。
RMI也提供了lookup service,用来寻找和访问远程对象。
看起来运行一样,实际上网络I/O是实际存在的,要考虑这些问题。

RMI称呼规定一下

客户辅助对象称为stub(桩),一般是RMI stub;
服务辅助对象称为skeleton(骨架),一般是RMI skeleton,新版java已经不这么叫,但差不多。

远程代理——以糖果机为例实现RMI

  1. 制作远程接口
    远程接口是要定义出让客户远程调用的方法,客户将用它作为服务的类类型,stub和实际的服务都要实现这个接口。

    import java.rmi.Remote;
    import java.rmi.RemoteException;
    //Remote定义一个记号接口,利用接口扩展接口
    public interface MyRemote extends Remote {
    /**
    * 所有方法必须抛出RemoteException,因为客户调用网络远程是需要网络I/O这些都是有风险的,
    * 所以直接在接口中声明异常让用户意识到这些危险
    *
    * 第二个就是远程方法的变量和返回值必须要实现Serializable接口
    */
    public String sayHello() throws RemoteException;
    }
  2. 制作远程的实现
    这是实际工作的类,比如糖果机就是为远程接口的远程方法进行了真正的实现。

    import java.net.MalformedURLException;
    import java.rmi.Naming;
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    /**
    * @author unclewang
    */
    /**
    * 因为这个服务实现就是实现MyRemote的,所以就是implements MyRemote
    * extends UnicastRemoteObject 扩展这个类可以让你直接变成远程服务对象,具备相应的远程功能
    *
    */
    public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    /**
    * 扩展UnicastRemoteObject类的代价就是构造器就是也会抛出相应的异常。因为父类也是这样
    */
    public MyRemoteImpl() throws RemoteException {
    }
    @Override
    public String sayHello() throws RemoteException {
    return "server says, 'hey'";
    }
    public static void main(String[] args) {
    try {
    /**
    * 你现在已经有远程服务了,为了让它被远程客户调用,你要将这个服务实例化,
    * 然后放进RMI registry(要保证RMI registry正在运行)
    * 注册方法就是:java.rmi.Naming类中的静态rebind()方法
    */
    MyRemote service = new MyRemoteImpl();
    Naming.rebind("RemoteHello", service);
    } catch (RemoteException e) {
    e.printStackTrace();
    } catch (MalformedURLException e) {
    e.printStackTrace();
    }
    }
    }
  3. 利用rmic产生的stub和skeleton
    利用rmic产生的stub和skeleton,这是java虚拟机自带的命令,我用idea的使用办法就是先run一下这个类,然后进入 /Users/unclewang/Idea_Projects/headfirst/target/classes 文件夹,然后执行 rmic headfirst.proxy_pattern.two.MyRemoteImpl 不过只生成了stub后缀的class,skeleton没生成。

  4. 启动RMI registry
    rmiregistry就像是电话本,客户可以从中查到代理的位置
    还是要进入 /Users/unclewang/Idea_Projects/headfirst/target/classes ,然后用终端启动 rmiregistry

  5. 开始远程服务
    先让服务对象开始运行,然后你的服务实现类会去实例化一个服务的实例,并将这个服务注册到RMI registry,注册之后,这个服务就可以供客户调用了。
    还是要进入 /Users/unclewang/Idea_Projects/headfirst/target/classes ,然后用终端启动 java headfirst.proxy_pattern.two.MyRemoteImpl

实现RMI后进行调用

客户端代码:

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class MyRemoteClient {
    public static void main(String[] args) {
        new MyRemoteClient().go();
    }
    public void go(){
        try{
            //使用远程接口MyRemote作为服务类型
            //使用lookup方法,其实Refistry提供的。
            //不需要port只需要提供注册时用的服务名字。
            MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
            String s = service.sayHello();
            System.out.println(s);

        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}

成功运行:

p.s. 我对SpringCloud理解的虽然也不是特别深,但是里面的Eureka做http服务配置发现感觉有点异曲同工,以后看看资料估计可以融会贯通。

动态类加载,以后细看

RMI容易犯错的三个地方:
1.忘了启动远程服务前先启动rmiregistry,这样才能进行绑定操作
2.忘了让变量和返回值的类型称为可序列化的类型
3.忘了给用户提供stub类

两边最后的配置

客户本地上应该有Client.class MyRemoteImpl_Stub.class MyRemote.class
服务器上应该有MyRemoteImpl_Stub.class MyRemoteImpl_Skel.class MyRemote.class MyRemoteImpl.class
服务器的绑定操作:Naming.rebind("RemoteHello", service) 其实绑定的就是Stub
客户也需要这个Stub类,虽然表面上客户和MyRemote在交互,但是Myremote是依靠Stub类操作的

用RMi修改糖果机,代码在gitee上

首先两台电脑(笔记本和台式机在同一个局域网里)都需要rmic headfirst.proxy_pattern.three.GumballMachine生成stub的class文件,也就是两边都有
我的步骤就是在笔记本上先通过 java headfirst.proxy_pattern.three.GumballMachineTestDrive unclewang 100
然后台式机直接运行GumballMonitorDriver,里面的IP是笔记本的ip,然后成功实时打印出来糖果机的数量;

  • 此外,我发现确实其实这个Stub就跟纽带一样,只要你两边都有这个,服务器可以自行修改自己的糖果机类,本地虽然修改要打印的东西,两边是独立的,然后重点就是绑定的地址和lookup的地址能够ping 通而且对应上就好。
  • 而且生成stub的class里面应该主要实现的就是客户端这边要操作的方法。
  • 甚至你可以在台式机上删除那些具体的实现类,只留下两个接口和Monitor类和MonitorTest类,依旧没有任何问题

定义代理模式

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
我的理解就是stub这个东西就是和socket很像,只是只绑定了ip,没有绑定端口的操作,两个通过ip和服务名称来进行通信,但通信的目的不是互传消息,而是为了本地这边调用自己的方法实际上可以访问对面的某些方法。

代理控制核心就是要创建代表,但根据具体情况还是分几种方式:
远程代理控制访问远程对象,上面的就是远程代理
虚拟代理控制访问创建开销大的资源
保护代理基于权限控制对资源的访问

代理模式类图以及介绍

代理模式类图

虚拟代理——从CD封面加载慢讲起

整个页面的某个地方加载有延迟,所以交给代理专门负责,这时候就可以用虚拟代理,虚拟代理帮助你不需要整个东西全部拿到才开始加载,而是先把下载的东西加载出来,等到图像或者什么难加载的东西加载好了,就替换掉虚拟代理原来呈现的东西。
所以虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中的时候,由虚拟代理来扮演对象的替身,对象创建后,代理就会将请求委托给对象。

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) {
            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(() -> {
                    try {
                        imageIcon = new ImageIcon(imageUrl, "CD Cover");
                        c.repaint();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
                retrievalThread.start();
            }
        }
    }
}



import javax.swing.*;
import java.net.MalformedURLException;
import java.net.URL;

public class ImageProxyTestDrive {
    ImageComponent jComponent;

    public static void main(String[] args) throws MalformedURLException {
        new ImageProxyTestDrive();
    }

    public ImageProxyTestDrive() throws MalformedURLException {
        JFrame frame = new JFrame();
        Icon icon = new ImageProxy(new URL("https://y.gtimg.cn/music/photo_new/T002R300x300M000003DFRzD192KKD.jpg?max_age=2592000"));
        jComponent = new ImageComponent(icon);
        frame.getContentPane().add(jComponent);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }
}

因为图片是从URL出来的,所以有网络传输的时间,所以直接paintIcon的时候先判断有没有这个,第一次访问肯定没有,所以会检索这个icon,检索到了直接再次repaint()就好

这个要重点看ImageIcon,ImageProxy,Icon
Icon负责显示图片,共有的方法从这来,就相当于GumballMachineRemote定义要沟通的方法;
ImageIcon是一个图片具体实现显示的类,相当于GumballMachine具体实现GumballMachineRemote接口
ImageProxy就像是那些Stub类,也实现了GumballMachineRemote接口,但是目的是在中间放着帮助你实现远程连接的需求。

  • Stub类也是实现了GumballMachineRemote接口的

虚拟代理 vs 装饰者 vs 适配者

首先虚拟代理和装饰者真的有点像,都是用一个对象把另一个对象包起来,然后把调用委托给Icon。但是根据以前的套路,不用想肯定是目的不一样啊。
装饰者为对象增加行为,而代理是控制访问,虽然控制访问也可以看成一种行为,但是两者是解耦的。。。


写不下去了,我真的觉得这个是代理思想套在了装饰者模式了,装饰者模式的装饰方法改成这种异步加载的感觉你说是虚拟代理一点问题也没有,不过是什么模式不重要,解决问题最重要。
不过有一点很重要,装饰者的被包着的依然可能会被创建,但是虚拟代理一般只创建包过之后的对象

虚拟代理和适配器像的原因是因为两者都是当在一个对象的前面,并负责请求的转发,但是适配器修改接口,虚拟代理不修改。

心里话:其实开始学了几个的时候,区分的很开,后来的问题变得复杂一些,慢慢的都会有些重合,很难是非A即B的,这本书的思想是从目的出发或者是一些模式具有的独特性出发,重点是问题都能被正确方便的解决。

保护代理——从相亲怎么修改信息来讨论

java在java.lang.reflect有自己的代理支持,利用这个包你可以在运行时动态的创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类是在运行时创建的,我们称这个java技术为动态代理。

java已经创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么,而且你不能想以前一样把代码放在Proxy了(因为你用java自带的,又改不了)。因此解决方案就是:你控制的代码就要放在InvocationHandle里。
java内置了InvocationHandler接口,他的工作就是利用反射相应代理的任何调用,你可以把InvocationHandle理解成代理收到方法调用后,做具体工作的对象。

背景:相亲可以打分

首先创建一个人的基本信息,把它设置成接口PersonBean,里面可以得到基本信息也可以修改基本信息包括自己的相亲分数。
这一听就知道很不合理,因为你可以把自己的分改的很高就不客观了,甚至别人都能修改你的名字。
我们希望是get大家都可以,但是对于set,自己的分数自己不能改,但是自己的名字别人不能改。

现在我们要为PersonBean创建动态代理

  1. 步骤一:创建两个InvocationHandle,分别是这个PersonBean的Own和NotOwn两种代理。
  2. 步骤二:写代码创建动态代理
  3. 步骤三:利用适当的代理包装任何PersonBean对象,使用已经写好的代理来处理具体情况
步骤一

首先关于InvocationHandle,这个里面只有一个invoke的方法,不管代理调用的是什么方法,一定是在invoke方法运行。
首先说明一下这个场景下,代理返回的就是上面的PersonBean类型,不过一会还是要使用Proxy.newInstance()再强制类型转换的。


InvocationHandle工作流程分为三步:

  • 当proxy调用某个方法的时候,比如proxy.setHotOrNotRating(9)时,你要明白proxy并不是真的有这个setHotOrNotRating方法
  • 而是进入了public Object invoke(Object proxy, Method method, Object[] args)这个方法,这个时候会自动进行映射

    proxy ==> Object proxy
    setHotOrNotRating ==> Method method
    9 ==> Object[] args

  • invoke的方法具体会执行如何转发给RealSubject,比如return method.invoke(person, args),这里的person虽然也是PersonBean的实例,但并不是proxy,而是实现了PersonBean接口的PersonBeanImpl类,上面如果是用这个method.invoke(person, args)返回,那么就没什么差别,但是可以通过判断去throw new IllegalAccessException()告诉person你没权限调用这个


上面这部分的相关代码:

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

public class NonOwnerInvocationHandler implements InvocationHandler {
    //把RealSubject要实例进来
    PersonBean person;

    public NonOwnerInvocationHandler(PersonBean person) {
        this.person = person;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (method.getName().startsWith("get")) {
                return method.invoke(person, args);
            } else if (method.getName().equals("setHotOrNotRating")) {
                return method.invoke(person, args);
            } else if (method.getName().startsWith("set")) {
                throw new IllegalAccessException();
            }
        }catch (InvocationTargetException e){
            e.printStackTrace();
        }
        //调用其他方法直接不理
        return null;
    }
}
步骤二&步骤三

这两步应该在一起,因为java已经有proxy类了,所以就是利用这个如何包装PersonBean对象让他在不同情况交给对应的InvocationHandle处理。
先放代码然后理解:

import java.lang.reflect.Proxy;

public class PersonProxyIntegration {

    public PersonBean getOwnerProxy(PersonBean person) {//设置Owner代理
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new OwnerInvocationHandler(person));
    }

    public PersonBean getNonOwnerProxy(PersonBean person) {//设置非Owner代理
        return (PersonBean) Proxy.newProxyInstance(
                person.getClass().getClassLoader(),
                person.getClass().getInterfaces(),
                new NonOwnerInvocationHandler(person));
    }
}

首先说明PersonProxyIntegration这个类并不是代理,实际的代理类就是Proxy类(肯定是Object的子类),然后就是我们写的方法需要一个Person对象作为参数,然后返回它的代理,因为代理Proxy和RealSubject有同样的接口(Proxy类被强制转换),所以返回类型和参数类型相同。
其次,利用Proxy创建代理,用它的静态newProxyInstance()方法生成真实的代理实例,三个参数依次是PersonBean的类载入器 person.getClass().getClassLoader(),代理需要实现的接口person.getClass().getInterfaces(),代理要用到的处理器 new NonOwnerInvocationHandler(person),这就是Proxy和InvocationHandle协同工作的原理。

用户真实操作以为创建的是PersonBeanImpl,实际创建的是这个代理方法返回的PersonBean,别人操作的对方也是这个返回的,完美解决。

测试

对同一个人实例出来不同的代理返回值,没有太多内容。

代理的其他形式

防火墙代理:控制网络资源的访问,保护主体免于“坏客户“的侵害。
智能引用代理:当主题被引用时,进行额外的动作。
缓存代理:为开销大的运算结果提供暂时存储
同步代理:在多线程的情况下为主体提供安全的访问
复杂隐藏代理:用来隐藏一个类的复杂集合的复杂度,并进行访问控制。
写入时复制代理:用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。

发表评论

电子邮件地址不会被公开。