zoukankan      html  css  js  c++  java
  • 设计之禅——深入剖析代理模式

    一、前言

    设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

    以上内容摘自百度百科。设计模式的重要性相信不用阐述了,在我看来,代码是一门艺术,设计模式则是完成这门艺术的必要骨架,它是一种思想,无关语言。
    我是学Java的,对于Java开发者而言有一个“春天”,那就是Spring框架。因其易用高效,极大程度得简化开发而迅速发展;同时Spring源码中也是大量运用设计模式,真正的将设计模式落地,因此也是学习设计模式的宝库。

    二、什么是代理模式?

    代理模式为一个对象提供一个替身或占位符以控制对这个对象的访问。

    这是《Head First 设计模式》对于代理模式的定义,而我最初对于代理模式的认知来自于Spirng。其AOP就依赖于动态代理来实现,也是面试必问题之一了,故而之前也是查阅了很多关于代理模式的资料;但网上的资料千篇一律,都是讲代理的实现,对于它与装饰者模式、适配器模式以及外观模式的区别不甚明了,于初学者而言或许会觉得这些模式都是通过委托来包装对象,借以增加额外的行为,甚至有时会认为它们就是一个模式。但如果你有看过《Head First设计模式》,其对于四者的区别有一个清楚的描述:

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

    假定你已经了解了这三个模式的实现,那么相信看到这里对于它们之间的区别应该很清楚了。抛开抽象的概念,它们之间本质的区别就在于所解决的问题不同,即适用场景的不同,也是真正的体现了设计模式即思想;因此,我们应该从思想的角度去理解实现设计模式,而非从功用上。

    回到代理模式,通过定义我们能够明白,代理模式提供被代理对象一个“代表”,用以控制客户对真实对象的访问;既然这个代表要能够“控制”访问,那么它是不是应该持有真实对象的所有资料(即对象的引用)呢?由此,我们可以总结出代理模式的三个特点:

    • 有两个角色参与,代理人与被代理人;
    • 代理人持有被代理人的引用;
    • 被代理人的行为不能或无法直接暴露给客户。

    代理模式应用场景非常广,包含且不限于远程代理、虚拟代理、保护代理、缓存代理等非常多的变体,但每一种变体的出现都是为了解决一种实际的问题,且都满足于上诉3个条件。像虚拟代理是为了避免直接访问创建开销大的资源,保护代理是基于权限控制对对象的访问等等,感兴趣的码友们可下去自行研究,接下来我们就从代理对象的创建方式来分析并实现静态代理及动态代理。

    三、如何理解并实现?

    设计模式的概念是从实际生活中抽象出来,每一个模式在实际生活中都对应很多的实例,因此,若是针对每一个模式都能在现实生活中举出至少三个实例出来,那么对于该模式的理解也就到位了。
    前文有提到代理模式必然包含“代理人”及“被代理人”两个角色且客户是无法直接使用被代理人提供的资源,这里以九城代理《魔兽世界》为例。魔兽老玩家应该都还记得当初魔兽代理权由九城换到网易那段时间,期间国内无法直接玩魔兽,需要连接国外服务器才行,但网络延迟非常的高,如果一直是这个情况,对于魔兽本公司暴雪而言必然会损失一大部分的客户,因此,找国内代理公司也就是必然了,也就有了九城和网易代理。但是,代理公司拿到的只是代理权,他能修改其本质吗?如果能,对于暴雪而言不会是一场灾难么?因此,代理人只能在原有的基础上增加自己的特色,这也就涉及到设计模式的六大原则之一了——“对扩展开放,对修改关闭”;虽然魔兽的代理运营权已经交给了九城,但对于玩家来讲,需要改变他原本的行为吗?肯定不需要改变才是最好的,那换到程序上而言,就是客户只需要调用代理人同样的方法,由代理人将请求委托给被代理人来真正执行,代理人可以在此过程中加入自己的逻辑来达到控制访问的目的,那代码中要如何去实现呢?
    uml
    通过上面的UML图不难理解只需要让代理人与被代理人实现共同的接口和方法就能轻松的实现一个代理模式,首先我们来看静态代理。

    (一)静态代理

    public interface Game {
    
        void operate(String companyName);
    
    }
    
    // 被代理人
    public class BlizzardGame implements Game {
    
        @Override
        public void operate(String compantName) {
            System.out.println("Game is operating by " + compantName + "!");
        }
    
    }
    
    // 代理人
    public class NineCity implements Game {
    
        private Game game;
    
        public NineCity(Game game) {
            this.game = game;
        }
    
        @Override
        public void operate(String companyName) {
            System.out.println("Localizate...");
            game.operate(companyName);
            System.out.println("Online activities...");
        }
    
    }
    
    public class MainClass {
    
        public static void main(String[] args) {
            Game proxy = new NineCity(new BlizzardGame());
            proxy.operate(proxy.getClass().getSimpleName());
    
        }
    
    }

    这段代码很简单,不必多说什么,主要就在于“针对接口编程”,同时代理人需要持有被代理人的引用。因其代理类及代理方法都是固定的,所以被称为“静态代理”。看起来非常不错,但是,我们这里只有一个方法,这样写没有什么问题,那如果有很多方法都需要代理,且代理的规则都是一样的呢?再者,如果不止一种代理规则呢?难道需要我们手动一个个地去创建代理类么?那是非常糟糕的一件事情,相信没人会那么干。既然我们不想耗费时间去做重复毫无意义的事情,那要怎样去创建代理呢?这时,动态代理就出现了。

    (二)动态代理

    动态代理的出现就是为了帮助我们减少重复工作,节省开发时间效率,以及让代码变得更赏心悦目。既然它这么厉害,那究竟是如何实现的呢?在java中动态代理有JDK自带以及CGLIB两种实现方式,首先我们来看JDK自带的方式:

    JDK实现及其原理

    1. 找代理人

    同样的这里先创建一个Subject接口,定义两个方法以示区分静态代理,并让代理人实现此接口:

     public interface Game {
    
        void publicize(String companyName);
    
        void operate(String companyName);
    
    }
    
    public class BlizzardGame implements Game {
        ......
    }

    接下来注意,与静态代理不同的是我们不需要手动创建一个代理类,而是需要写一个Handler来实现InvocationHandler接口,代理类则是由程序运行时动态的生成,这个Handler则可以理解为代理类的辅助类,也就是如何定义代理的规则,调用的所有的代理方法最终都会进入到这里面。

    public class MyInvocationHandler implements InvocationHandler {
    
        // 被代理人的引用
        private Game game;
    
        public Game getInstance(Game game) {
            ......
        }
    
        // 所有代理的方法最终调用的方法
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            ......
        }
    }

    我们创建自己的Handler,并定义getInstance方法,传入被代理人的实例并将其引用赋给成员变量,接着再通过newProxyInstance让JDK自动为们生成以$Proxy0(0是编号,若有多个代理类,即多个Handler则会依次递增)命名的动态代理类保存在内存中(动态的生成类),客户通过调用该方法获取到代理类的实例。

     this.game = game;
    
     Class clazz = game.getClass();
     Game instance = (Game) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    

    然后重写InvocationHandler中的invoke方法,也就是定义代理的规则,将共同的逻辑抽离出来实现代码复用(动态的调用方法),即每个代理方法最终都会调用的方法。

    // invoke包含了三个参数,proxy就是生成的代理类实例,method是正在执行的代理方法,args则是方法的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
        System.out.println("Before operate...");
        // 此处的game不能换成proxy,否则会形成死循环,为什么?先想想,稍后会将
        Object invoke = method.invoke(game, args);
        System.out.println("after operate...");
    
        return invoke;
    }

    测试

    MyInvocationHandler handler = new MyInvocationHandler();
    // Get proxy object
    Game instance = handler.getInstance(new BlizzardGame());
    // 这里就是动态的调用代理方法
    instance.publicize("Nine City");
    instance.operate("Nine City");

    相信看到这儿对于动态代理是如何帮助我们实现多种代理以及如何代理多个方法已经很清楚了,代码很简单,我们只需要调用JDK的API就行了,而底层复杂的实现方法都由JDK做了,那JDK到底是怎么做的呢?生成的代理类在哪里,长啥样?我们都不知道啊,作为程序员一定要知其然还要知其所以然。

    2. 代理人长啥样?

    要了解其原理,那么看其源码肯定是最有效的,而动态代理类底层是通过字节码及反射技术生成的并保存在内存中,我们可以通过Proxy本身提供的方法generateProxyClass方法来生成.class文件,再反编译即可看到。

    byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{instance.getClass()});
    FileOutputStream fo = new FileOutputStream("$Proxy0.class");
    fo.write(bytes);
    fo.close();

    将上面的代码放到测试代码最后则会在根路径下生成一个$Proxy0.class文件,如下:

    public final class $Proxy0 extends Proxy implements Proxy0 {
    
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
    
        public final void publicize(String var1) throws  {
            try {
                super.h.invoke(this, m3, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
    
        public final void operate(String var1) throws  {
            try {
                super.h.invoke(this, m4, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
    
    }

    为节省篇幅我删掉了来自于Object的方法。首先我们看其构造方法,我们可以明白newProxyInstance传入的InvocationHandler就是在这里用来初始化Proxy类中的InvocationHandler的。

    public class Proxy implements java.io.Serializable {
    
    protected InvocationHandler h;

    再看两个代理方法,客户端在调用相应方法时其实是由代理类再委托给我们自己写的Handler,最终都是执行其invoke方法,并将代理对象传入进去,那么上文提及的死循环问题相信也都理解为什么出现了,至此我们对于动态代理是如何做到的应该都清楚了,但是代理类是如何生成?又是如何保存到内存中的呢?下面我们就自己来实现一个。

    3. 成为创造者

    首先创建MyProxy、CustomizeClassloader、CustomizeInvocationHandler三个类替换掉JDK自带的Proxy、Classloader、InvocationHandler:

    public class MyProxy {
        // 这里就是我们自己来生成代理类的逻辑
        public static Object newProxyInstance(CustomizeClassLoader loader,
                                              Class<?> interfaces,
                                              CustomizeInvocationHandler h)
                throws IllegalArgumentException {
    // 继承自ClassLoader并重写其findClass方法
    public class CustomizeClassLoader extends ClassLoader {
    public interface CustomizeInvocationHandler {
    
        Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable;
    
    }

    创建完成之后我们就需要对newProxyInstance来分析,为什么客户直接调用这个方法就能或取到代理类对象?应该如何去实现?下面是根据网上大牛们的博客进行总结的:

    首先我们需要生成$Proxy0中的代码;
    其次,创建.java文件,并将第一步生成的源码写到里面;
    第三,将.java文件编译为.class文件;
    第四,将class加载到jvm;
    最后,只需要通过反射生成代理类返回就行了。

    这个逻辑是否正确?往下看之前先认真想想有没有什么问题。虽然按照这个套路是可以实现的,但是如果需要生成大量的代理类时,性能会不会存在什么问题?
    带着这样的疑问,我将这种实现和JDK源码做了比较,发现JDK底层是通过直接操作字节码来完成的,因此性能上是没有太大问题的,限于小编目前的水平还达不到操作字节码的程度,就按上述逻辑实现了,由于篇幅原因,这里就不过多阐述,具体源码请移步小编github仓库。点击查看

    CGLIB实现

    讲到这篇幅已经很长了,但在Spring中并不只是使用了JDK动态代理来实现AOP,还有CGLIB实现,那为什么又会出现这种实现方式呢?他们之间有什么区别呢?
    要实现CGLIB需要引入cglib包

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.4</version>
    </dependency>

    然后我们同样创建一个被代理人,此处不需要再实现自接口

    // 被代理人
    public class BlizzardGame {......}

    与JDK一样的是我们肯定也需要代理人与被代理人两个角色,而代理人同样是通过程序生成;不同的是,CGLIB需要我们写一个自己的拦截器实现MethodInterceptor,它的作用其实和InvocationHandler是一样的,也就是辅助创建代理类并定义代理的规则。

    public class MyInterceptor implements MethodInterceptor {
    
        public Object getInstance(Object obj) {
            // 代理类生成器
            Enhancer enhancer = new Enhancer();
            // 设置代理类的父类,即被代理人
            enhancer.setSuperclass(obj.getClass());
            // 设置回调
            enhancer.setCallback(this);
    
            return enhancer.create();
        }
    
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("before...");
            // 这里需要调用父类的方法,也就是被代理人的方法,否则会死循环
            Object o = methodProxy.invokeSuper(obj, args);
            System.out.println("after...");
    
            return o;
        }
    }

    代码依然很简单,其生成代理类的原理同JDK生成代理类的原理其实是差不多的,只不过,JDK需要代理人和被代理人都实现同一个接口,而CGLIB则是直接生成被代理人的子类来实现代理。这也就是CGLIB产生的原因,针对没有实现接口的类就可以采用这种方式来实现动态代理。而对于网上所说的CGLIB的效率JDK的10倍,小编在JDK1.8环境下测试过,发现无论如何JDK的效率都是远远高于CGLIB的,我想或许是JDK版本提升后也优化了动态代理的实现吧。感兴趣的可下去自行测试。

    四、总结

    代理模式实现方式很简单,可以帮助我们解耦合以及控制对象的访问,但也会显然地提升程序处理时间,因此也不要盲目的使用。
    至此,结束!小编的Github地址:https://github.com/smile-everyday。欢迎关注评论!最后推荐谷歌的一个插件:Insight.io for github。可以让你在github上浏览代码如IDE中那么方便。下面是效果图:
    这里写图片描述
    下载地址:https://pan.baidu.com/s/1O519iW03lf1TpAdDzkISwA 密码:gfu6

  • 相关阅读:
    从零开始学SQLSERVER-游标CURSOR(基础)
    从零开始学SQLSERVER-创建表
    从零开始学HTML-button
    从零开始学前端HTML-input标签
    专项测试实战 | 如何测试 App 流畅度(基于 FPS 和丢帧率)
    接口测试框架实战 | 流程封装与基于加密接口的测试用例设计
    软件测试之30分钟轻松搞定正则表达式基础
    Python 装饰器填坑指南 | 最常见的报错信息、原因和解决方案
    react16与react15之间的区别
    react-父子组件之间的交互传值
  • 原文地址:https://www.cnblogs.com/yewy/p/13111854.html
Copyright © 2011-2022 走看看