• 设计模式之代理模式


    代理模式为另一个对象提供一个替身以控制对这个对象的访问。从定义可以看出,1. 代理模式提供了一个替身,即代理对象 2. 代理对象是为了控制对另一个对象(真实对象)的访问,控制可以理解为做权限检查、可行性判断等。举个例子,代理对象 = 经纪人,真实对象 = 明星,如果某剧组想邀请明星出演电影,先将剧本给经纪人,经纪人先判断剧组的真实性以及剧本的价值,如果是无良剧组或者垃圾剧本直接怼回去,这便是控制。如果剧组和剧本靠谱便转交给明星处理,明星确定演不演把结果反馈给经纪人,经纪人再反馈结果给剧组,这个过程就可以理解为代理模式。代理模式有很多种,包括动态代理、远程代理、虚拟代理等等,本章我们详细介绍动态代理(Java版),简单介绍远程代理和虚拟代理。

    动态代理

    之所以叫动态是因为运行时才将代理类创建出来。我们先由一个简单的需求引入动态代理技术,同时也会介绍面向对象设计原则。需求如下:在我们的业务当中,需要将某些数据写入本地磁盘做持久化,因此程序中需要封装一个写文件的类来满足业务需求。前期的业务很简单,我们只需要定义能够提供写入文件的方法即可。因此,首先定义一个 Writer 接口,包含不同的写方法,其次定义一个该接口的实现类,实现该接口定义的方法。

    package com.cnblogs.duma.dp.proxy.dynamic;
    
    public interface Writer {
        public void write(String fileName, String str);
        public void write(String fileName, byte[] bs);
    }
    package com.cnblogs.duma.dp.proxy.dynamic;
    
    public class FileWriter implements Writer {
        @Override
        public void write(String fileName, String str) {
            System.out.println("call write str in FileWriter");
        }
    
        @Override
        public void write(String fileName, byte[] bs) {
            System.out.println("call write bytes in FileWriter");
        }
    }

    之后我们用 Writer writer = new FileWriter();  就可以完成向本地文件写数据的功能了。这里其实不定义接口也能实现这个功能,至于为什么要定义接口下文会有解释。至此,我们的小需求完成了,也上线了并能正常运行。突然有一天运维小哥说了,为了保证 xxx ,需要在服务器预留 100G 磁盘空间, 也就是说我们的应用程序写磁盘的时候要判断已有的磁盘空间,如果快到了 100G 临界值,就不能再写了。因此,我们需要改代码,写之前加上一个判断当前可用的磁盘空间的逻辑,本来我们可以直接改 FileWriter 的代码。但存在两个问题 1. 改现有代码风险高,可能改动过程中影响原有逻辑,并且要重新进行单元测试 2. 这个需求比较牵强,跟我们的实际业务无关,直接放在业务代码里面导致耦合度比较大,不利于维护。因此,我们可以考虑使用代理模式解决这个问题,即可以保证现有代码不动,又可以低耦合地实现目前的需求。

    package com.cnblogs.duma.dp.proxy.dynamic;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class FileWriterInvocationHandler implements InvocationHandler {
        Writer writer = null;
    
        public FileWriterInvocationHandler(Writer writer) {
            this.writer = writer;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
            boolean localNoSpace = false;
            System.out.println("check local filesystem space."); //检测磁盘空间代码,返回值可以更新 localNoSpace 变量
            if (localNoSpace) {
                throw new Exception("no space."); //如果空间不足,抛出空间不足的异常
            }
            return method.invoke(writer, args); //调用真实对象(FileWriter)的方法
        }
    }

    可以看到只增加了一个类,这个类有个特点 1. 它实现了 InvocationHandler 接口 2. 它的 invoke 方法实现了我们的需求并控制是否要调用真实对象。InvocationHandler 是 Java 动态代理定义的一个接口,接口中定义了一个 invoke 方法,我们调用代理对象的任何方法都会变成对 FileWriterInvocationHandler 对象的 invoke 方法的调用, invoke 方法就是代理要做的事情。如果看到你觉得一头雾水,没关系继续向下看将豁然开朗。

    到目前为止我们只看到新增了一个 InvocationHandler 接口的实现类,并没有看到代理对象。之前说过之所以是动态代理是因为在运行时才创建代理类,因此我们需要编写一个驱动程序,动态创建代理对象,完成动态代理的后半部分。

    package com.cnblogs.duma.dp.proxy.dynamic;
    
    import java.lang.reflect.Proxy;
    
    public class DynamicProxyDriver {
        public static void main(String[] args) {
            /**
             * Proxy.newProxyInstance 包括三个参数
             * 第一个参数:定义代理类的 classloader,一般用被代理接口的 classloader
             * 第二个参数:需要被代理的接口列表
             * 第三个参数:实现了 InvocationHandler 接口的对象
             * 返回值:代理对象
             */
            Writer writer = (Writer) Proxy.newProxyInstance(
                    Writer.class.getClassLoader(),
                    new Class[]{Writer.class},
                    new FileWriterInvocationHandler(new FileWriter())); //这就是动态的原因,运行时才创建代理类
    
            try {
                writer.write("file1.txt", "text"); //调用代理对象的write方法
            } catch (Exception e) {
                e.printStackTrace();
            }
            writer.write("file2.txt", new byte[]{}); //调用代理对象的write方法
        }
    }

    最关的一步是 Proxy.newProxyInstance ,该调用会创建代理对象,该代理对象会将我们需要代理的接口(Writer)和 InvocationHandler 实现类关联起来。这样代理对象就会有 Writer 接口的 2 个方法,针对我们的业务逻辑调用过程为:调用代理对象 writer 的 write 方法写数据 -> 转到 FileWriterInvocationHandler 对象的 invoke 方法,判断磁盘空间是否够用 -> 抛出磁盘空间不足异常或调用 FileWriter 对象的 write 方法写数据。在这里动态代理涉及到了 Writer 接口及其实现类、InvocationHandler 接口及其实现类、代理类。动态代理 UML 类图如下:

    可以看到代理类 Proxy 实现了 Writer 接口,因此可以调用 write 方法,同时代理类关联 FileWriterInvocationHandler ,因此对 write 方法的调用会变成对 invoke 方法的调用。

    至此,新的需求就完成了,我们结合代理模式谈谈此次需求变更我们用到了哪些好的设计原则。

    1. 我们没有在原有 FileWriter 实现类中修改代码, 而是新增了 FileInvocationHandler 实现新需求,这符合设计原则中的开闭原则,即:对扩展开发对修改封闭。改动现有代码容易影响已有的正常代码

    2. 我们增加代理之后只是把 Writer writer = new FileWriter() 改为 Writer writer = Proxy.newProxyInstance(...),由于都继承了 Writer 接口,因此不需要修改 writer 的类型, 这符合面向接口的设计原则,让我们尽量少的改动现有代码

    动态代理还有一个重要的应用场景,我们可以在 invoke 方法中把待调用的方法名(method)和参数(args)发送到远程服务器,在远程服务器中完成调用并返回一个结果,这其实就是 RPC (remote procedure call),即:远程过程调用。我在阅读 Hadoop 源码过程中发现 Hadoop RPC 将动态代理技术应用在上述场景中。

    远程代理 

    个人觉得上述动态代理第二个应用场景算是远程代理的一个特例,因为远程代理不一定非要动态创建代理对象。接下来我们以 Java RMI 为例, 简单看下远程代理。RMI(remote method invocation)即:远程方法调用,与 RPC 类似,可以让我们像调用 Java 本地方法一样,调用远程的方法。这里就需要一个代理对象,它实现了本地的接口,其中序列化/反序列化以及网络传输都在代理对象中实现, 对我们透明,这也是控制了我们对远程对象的访问。代码如下:

    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    /**
     * 定义一个接口,接口中的方法要在远程调用
     */
    public interface MyRemote extends Remote {
        public String sayHello() throws RemoteException;
    }
    import java.net.MalformedURLException;
    import java.rmi.Naming;
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    
    /**
     * 定义一个接口的远程实现类
     * 为了让远程对象拥有 “远程的” 功能,需要继承 UnicastRemoteObject 类
     */
    public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    
        protected MyRemoteImpl() throws RemoteException {
        }
    
        /**
         * 客户端通过 rmi 代理对象调用 sayHello 方法,将会进入到此方法
         * @return
         * @throws RemoteException
         */
        @Override
        public String sayHello() throws RemoteException {
            System.out.println("req from client.");
            return "Server says, 'Hey'";
        }
    
        /**
         * 启动远程进程的 main 方法
         * @param args
         */
        public static void main(String[] args) {
            try {
                MyRemote service = new MyRemoteImpl();
                Naming.rebind("RemoteHello", service); //将服务名和对应的服务进行绑定,客户端会根据 RemoteHello 找到远程服务
            } catch (RemoteException e) {
                e.printStackTrace();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
    }

    这样我们的远程服务已经写好了,还需要做以下 3 个工作来启动远程服务

    1. 生成客户端代理类,需要在 MyRemoteImpl.class 所在的目录中执行 rmic MyRemoteImpl 命令,将会生成 MyRemoteImpl_Stub.class 类。首先,rmic 命令是 jdk 自带命令,所在的目录与 java 和 javac 所在的目录一样;其次,我用的 Idea 创建的普通 Java 工程,我的 MyRemoteImpl.class 文件在“E:ackendsjava-backendsjava-exoutproductionjava-ex”目录中,以我的工程为例,路径以及命令执行如下:

    E:ackendsjava-backendsjava-exoutproductionjava-ex>rmic MyRemoteImpl

    2. 启动 rmiregistry,为了远程服务可以注册服务名,在我们的 class 所在的目录(“项目目录outproductionjava-ex”)中执行 rmiregistry 命令

    E:ackendsjava-backendsjava-exoutproductionjava-ex>rmiregistry

    3. 运行 MyRemoteImpl 类,启动远程服务进程

    继续编写客户端访问代码,客户端代码主要是找到刚刚注册的 RemoteHello 远程服务,并获得代理对象,调用代理对象上的方法。我们可以在同一个工程下,创建 MyRemoteClient 类

    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) {
            try {
                /**
                 * 找到远程服务,并返回代理对象
                 * 该代理对象就是 MyRemoteImpl_Stub 且实现了 MyRemote 接口
                 */
                MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
                /**
                 * 调用代理对象的 sayHello 方法,便会通过代理将调用发送到远程服务进程并返回结果
                 */
                String ret = service.sayHello();
                System.out.println(ret); 
            } catch (RemoteException e) {
                e.printStackTrace();
            } catch (NotBoundException e) {
                e.printStackTrace();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
    }

    我们可以直接运行 MyRemoteClient 类,可以看到在刚启动的 MyRemoteImpl 进程中,控制台打印了 

    req from client.

    在 MyRemoteClient 进程的控制台中打印了

    Server says, 'Hey'

    至此我们的远程代理已经介绍完毕。

    虚拟代理

    虚拟代理是作为创建开销大的对象的替身。举一个我们常见的例子,在 Web 开发或者移动端开发的时候经常会用到 Image 组件,Image 组件一般要传入一个 URL 参数,从网络上下载图片到本地展示。假设这个组件要等到图片下载完成才有显示,那如果图片较大或者网络较慢,给用户造成不好的体验。解决方法是我们可以先显示一个 loading 状态的默认的本地图片,当远程图片下载完成后重新渲染,替换掉当前的 laoding 状态的图片。用虚拟代理来实现这个技术就可以定义一个 ImageProxy 类型,在该类中初始时候先展示一个默认图片,启动线程创建 Image 对象,Image 对象创建完毕,再重新渲染,替换默认图片。虚拟代理也是控制了对 Image 对象的访问。

    总结

    本章主要介绍了代理模式,并且我们看到了代理模式常用的几种变形,同时也接触了面向对象的基本的设计原则

    动态代理 - 程序运行时动态地创建代理对象,所有的对代理对象方法的调用都会变成对 InvocationHandler 的 invoke 方法的调用

    远程代理 - 本地调用代理对象访问远程的方法,无需关心网络通信细节,跟调用本地方法一样

    虚拟代理 - 为了创建开销大的对象而存在

    可以看到代理模式最核心就是控制,代理对象的目的就是控制对真实对象的访问。

    本章主要参考《Head First 设计模式》

  • 相关阅读:
    单工-半双工-双工
    为你的Windows7设置动态壁纸
    Vmware为Ubuntu安装VmTools
    CodeBlocks集成cppcheck
    自定义鼠标右键(层叠式菜单:cascading menu)
    Hao123这个流氓
    Android的ADT内容助手快捷方式设置
    安装Google框架服务并突破Google Play下载限制
    Windows7下CHM电子书打开不能正常显示内容
    谷歌首页背景设置
  • 原文地址:https://www.cnblogs.com/duma/p/10629302.html
Copyright © 2020-2023  润新知