细聊代理模式

如果要说设计模式中哪种模式在生活中最常见,那么代理模式是当仁不让的。比如,你一觉睡到中午不想去食堂打饭,于是你委托阿黄让他帮你把饭带回来,这就产生了一种代理关系,其中委托人是你,代理人是阿黄,而事件就是打饭,你为了足不出户就能吃到午饭而想到的这馊主意,就是一种代理方法。又比如,你女朋友从外地回来下飞机了,而你又因为一些不可描述的事情不能去接机,这时候你就可以叫你的好基友去机场帮你把女朋友接回来啊,于是你和你好基友就形成了一种代理关系,委托人是你,代理人是好基友,而事件就是接女朋友回来,这种很有可能戴绿帽子的方法就是一种代理方法。然后,将这些代理方法总结规律,抽象出来,上升到一个通用层面的高度,就是代理模式。什么是代理模式呢?通俗地讲,有些事情本应该是你做的,但是你不想做或者不方便做的时候,叫别人替你完成。生活中种种“帮帮忙”的事件,都有代理模式的影子,所以说代理模式在生活中很常见。

静态代理

那么我们如何使用代码来描述代理模式呢?我们将上面的例子总结规律,抽象出来,就大概有下面两点:

  • 委托人和代理人都要完成同样的事情(共同实现事件的接口)。
  • 代理人要做的这件事其实是委托人请求的,也就是说,代理人本身并不知道如何做这件事,只有委托人告诉他了,代理人才知道怎么做。所以,代理人需要持有委托人的引用,在做这件事的时候就按照委托人的意愿去做。

就比如第一个例子,“打饭”这件事情,由于你和阿黄都能做,所以可以写成一个接口,让你和阿黄都能实现。

/**
 * 一个抽象的午饭类, 具体要吃什么样午饭, 看子类的心情。
 */
public interface Lunch {

    /**
     * 吃什么样的午饭, 糖醋排骨?宫保鸡丁?。。。。
     */
    void catagory();
}

然后就要创建“你”这个对象了,由于你要吃午饭,就需要实现Lunch类:

/**
 * 你, 需要考虑午饭吃什么
 */
public class You implements Lunch {
    @Override
    public void catagory() {
        // 你想吃宫保鸡丁
        System.out.println("我要吃宫保鸡丁盖饭");
    }
}

然后你很懒啊,需要找阿黄这个代理人帮你带午饭回来,所以还需要一个代理人对象,注意,由于代理事件是委托人(“你”)下发的,所以代理人对象里面还需要持有委托人的应用:

/**
 * 代理人, 由于需要帮委托人带饭, 也必须实现午饭接口
 */
public class ProxyMan implements Lunch {
    /**
     * 必须是要有委托人的存在才行,没有委托人还需要代理人干嘛?
     */
    private You mYou;
    /**
     * 在创建代理人对象的时候就传入一个委托人进来,这样就使得代理人可以持有委托人的引用了。
     * 当然这只是一种方法,你也可以使用其他方法让代理人持有委托人的引用。
     */
    public ProxyMan(You you) {
        mYou = you;
    }
    @Override
    public void catagory() {
        // "你"要让代理人帮你做的事情。
        mYou.catagory();
    }
}

OK,大功告成,然后我们创建一个场景(客户端)来模拟一下这个事情:

public class Proxy {

    public static void main(String[] args) {
        // 创建一个委托人
        You you = new You();
        // 创建一个代理人, 可以是阿黄, 也可以是其他人
        ProxyMan aHuang = new ProxyMan(you);
        // 让阿黄帮你带饭。
        aHuang.catagory();
    }
}

以上便是代理模式的一种模版,结合代码,我们可以总结出代理模式的类图如下:

Class Diagram (1).png-12.5kB
Class Diagram (1).png-12.5kB

好了,扯了这么多,大概对代理模式有一个印象了。是时候给代理模式来下一个正式的定义了:为其他对象提供一种代理以控制对这个对象的访问。

另外,我们现在看到的代理模式,又称之为静态代理,因为代理类是需要我们自己手动编写的,后面我们会谈谈如何不用手动编写代理类,来实现代理模式。

静态代理的作用

当然,你会说,这种小事,委托类自己就能完成的嘛,非要多增加一些类干嘛。的确,如果单是这种简单的例子,使用任何设计模式都是多此一举。但在实际工作中,使用代理模式就会有如下的有点:

  • 解耦。这几乎是设计模式解决的根本问题。在这里,委托类可以专心地做好自己,麻烦的事情让别人代劳。
  • 拦截、扩展方法。代理类在实现接口方法的时候,除了直接调用委托类的方法外,还可以在不修改委托类的情况下,增加一些其他的功能,比如顺便带瓶可乐回来?

为了能让我们的代理人阿黄能带瓶可乐回来,我们只需要在代理类中对catagory()方法增加一个“带瓶可乐回来”的功能即可:

public class ProxyMan implements Lunch {
   
    private You mYou;
  
    public ProxyMan(You you) {
        mYou = you;
    }
    @Override
    public void catagory() {
        // "你"要让代理人帮你做的事情。
        mYou.catagory();
        
        // 打完饭后再去买瓶可乐
        System.out.println("嗯,买瓶可乐来喝");
    }
}

现在你应该能明显地感受到代理类的作用了。对,就是拦截方法、对委托的方法进行加工处理。

动态代理

然后有一天,当你中午醒来的时候,发现寝室里除了你以外空无一人,你为了双倍经验双倍金币所以不想浪费时间下楼去食堂打饭,但是你饿啊,要吃饭啊,这时你就只有等寝室再进来个人,让他帮你带饭(假设你不会叫外卖)。但是这会有一个问题,之前一醒来就可以让阿黄帮你带饭,是因为阿黄在你醒来之前就在寝室的(程序编译阶段就存在一个代理类),并且设定了阿黄“能打饭”(实现了Lunch接口)这个属性,所以简单的几行代码就能搞定。但是现在,你醒来之后(程序开始运行),发现并没有人可以帮你带饭(没有一个在编译阶段就实现了Lunch接口的代理类),在你等待的过程中也并不知道会是谁进来,阿黄,阿猫还是阿三(我们需要在程序运行的时候动态生成一个对象)?而且最重要的是,进来的那个家伙能帮你带饭才行(运行阶段生成的对象要实现Lunch接口)。

那么我们再梳理一遍,现在的你应该怎么做才能吃到午饭呢?

  • 首先,“你”这个对象和“午饭”这个事件是必须存在的。
  • 其次,创建一个代理人。由于这个代理人是帮你做事情的,所以创建这个代理人的方法必须和“你”这个对象以及“午饭”事件接口相关。
  • 最后,让代理人去帮你打饭回来。

让我们来用代码实现一下,You这个委托类和Lunch接口是要保留的,而ProxyMan这个代理类就不需要了。在场景中(client客户端),我们可以使用代理的一个静态方法newProxyInstance来创建一个代理对象,这个代理对象当然是实现了Lunch这个接口的。那么client中的代码如下:

public static void main(String[] args) {
    // 创建一个委托人
    You you = new You();
    // 创建一个代理人, 第一个参数是委托类的ClassLoader, 第二个参数是要实现的接口数组,
    // 第三个参数是一个匿名内部类, 对于代理人将要做的任何事情做拦截处理。
    Lunch somebodyCouldTakeLunch = (Lunch) Proxy.newProxyInstance(you.getClass().getClassLoader(), new Class[]{Lunch.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return method.invoke(you, args);
        }
    });
    //  动态生成的代理人去带饭了
    somebodyCouldTakeLunch.catagory();
}

当动态生成的代理人对象调用方法去带饭时(somebodyCouldTakeLunch.catagory()),就会触发匿名内部类InvocationHandler中的invoke()方法,这里面没有做其他处理,直接返回了“你”这个对象的对应方法。也就是说,动态生成的代理人是安装你的要求带回来宫爆鸡丁盖饭!

一个简单的动态代理就这么完成了。然而动态代理是想当强大的,为了显示这种强大,这里再举一个例子,List集合大家都用过,这个接口有很多方法,如果我们想要屏蔽remove这个方法,这时我们就可以使用动态代理,来生成一个代理类List,每当调用到代理类的remove()方法时,就跑出异常,实现代码如下:

public static List getList(List list) {
        return (List) Proxy.newProxyInstance(List.class.getClassLoader(), new Class[]{List.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("remove")) {
                    throw new UnsupportedOperationException();
                } else {
                    return method.invoke(list, args);
                }
            }
        });
    }

当我们传入一个List对象进去,返回的就是经过处理后的代理List类,这时如果调用代理List的remove方法时就会报错了。

估计你已经知道了如何使用动态代理了,但是你肯定有很多疑惑,Proxy.newProxyInstance这个静态方法是怎么完成这件事的呢?我们看一下源码(以下分析的源码是基于jdk 1.6,相比起来比较简单易懂):

public static Object newProxyInstance(ClassLoader loader, 
            Class<?>[] interfaces, 
            InvocationHandler h) 
            throws IllegalArgumentException { 
    
    // 检查 h 不为空,否则抛异常
    if (h == null) { 
        throw new NullPointerException(); 
    } 

    // 获得与制定类装载器和一组接口相关的代理类类型对象
    Class cl = getProxyClass(loader, interfaces); 

    // 通过反射获取构造函数对象并生成代理类实例
    try { 
        Constructor cons = cl.getConstructor(constructorParams); 
        return (Object) cons.newInstance(new Object[] { h }); 
    } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); 
    } catch (IllegalAccessException e) { throw new InternalError(e.toString()); 
    } catch (InstantiationException e) { throw new InternalError(e.toString()); 
    } catch (InvocationTargetException e) { throw new InternalError(e.toString()); 
    } 
}

其他都好懂,可能就是getProxyClass()这个方法没见过。同时该方法也是动态代理中最核心的东西。由于该方法比较长,去除注释后也有120+行代码,所以我就不粘贴代码了,你可以自己去Java源码中去寻找,或者你可以查看这个链接: getProxyClass(),里面大概的流程是:先将传入的接口数组进行一系列检查,并存储所有接口名称,然后创建一个HashMap的缓存表,里面以不同的键值对形式来存放着待创建的代理类、正在被创建的代理类。然后根据一系列的规则确定包名,调用ProxyGenerator.generateProxyClass()方法创建代理类,这个方法会调用ProxyGenerator对象的generateClassFile()方法来生成代理类类文件的字节数组,具体生成方法可以参见这个链接中的源码: generateClassFile(),我就不再深入阐述了。生成代理类对象后,将其记录到proxyClasses表中,然后就是更新缓存表、清楚缓存表等一系列善后工作了。

由于所有的代理类都有一个共同的父类Proxy,Java 的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在 Java 中本质上就行不通。所以Java只能进行接口的动态代理,这不得不说是一个遗憾。

静态代理和动态代理的联系

然而在大多数情况下,我们的代理类在敲代码的时候就能确定下来,也就是说,我们日常开发的大多数情况都能使用静态代理。那么能不能用动态代理呢?当然能!为什么要用动态代理呢?

当委托类实现的接口方法比较多的时候,写一个代理类一个个地处理委托方法就太麻烦了。这时我们可以在invoke()方法中对委托类的所有方法进行判断、拦截、扩展等处理。减少代码量。

Retrofit中的动态代理

Retrofit可谓是当下最火的Android网络请求框架之一了,我们在Retrofit的wiki上可以看到Retrofit的使用方法如下:

1.创建一个请求方法的接口:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

2.生成Retrofit对象,并且创建一个实现了GitHubServiece接口的实体类:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

3.发起网络请求:

Call<List<Repo>> repos = service.listRepos("octocat");

我们看第二步,传入了一个接口对象,然后就创建了一个实现了GitHubService接口的实体类?怎么实现的?Excuse me?这时候,就需要轻轻地点击去看源码:

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {

              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod serviceMethod = loadServiceMethod(method);
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
}

一个大写的动态代理!!!当然这里用的并不是规范的代理模式,因为我们知道,代理模式是需要有一个具体的委托类的,但是这里并没有一个具体的委托类,它是直接将传入的GitHubService接口,既作为事件接口,又作为代理类使用,显然这里拦截了接口中的方法,并不能对该接口的方法本身进行任何操作,这里使用动态代理,纯粹是为了拦截方法,获取接口方法上的注解信息,然后返回一个Adapter。这种可以算得上一个动态代理的扩展应用吧。

静态代理和动态代理的应用场景很多,常见的框架都有用到(比如Spring框架、OrmLite框架),希望读完本篇文章能帮你理解静态代理和动态代理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容

  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,922评论 1 15
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李头阅读 3,232评论 2 10
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,930评论 6 13
  • 开始学习做产品的第一天。 忍不住吐个槽,简书能不能把这个新建文章栏做成鼠标不在此栏时自动隐藏... 从...
    duXinG丶阅读 192评论 0 1
  • 我的发小、老同学耿玲霞的感人事迹,能得到郭柏生、仇建军、宋松柏、宋亚琴、耿同心、耿有民、高娟、郭亚玲、范秉辉...
    耿平海阅读 821评论 9 8