JAVA 设计模式之《代理模式》

一、代理模式介绍

代理模式,非常容易理解,一眼看过去大家都懂,字面意思,如果不懂稍微琢磨琢磨就懂了,我在这里举几个栗子🌰 ,以便更容易理解。

  • 贾乃亮作为一个明星,为了更专注于自己的工作,他找了一个经纪人,帮他约综艺、约电视剧、约电影等等。
  • 英雄联盟打的太菜了,但是为了冲到更高的段位,在别人面前装装13,于是找了一个代练帮忙上分。
  • 背井离乡,来到一个陌生的城市打拼,不知道住在哪里怎么办?我们可以通过中介更快的找到心仪的住所。
  • 春运的票实在太难抢,我们只能使用抢票软件帮我们抢票。

可以看到,在生活中,代理无处不在,无论是经纪人、游戏代练、房产中介、抢票软件,都是帮我们做事,对我们的能力进行了扩展。

那么在Java中使用代理模式,通过代理对象访问目标对象,从而在不改变目标对象原有代码的基础上,进行功能的扩展和增强。

目前在Java中分为三种代理模式:静态代理、jdk动态代理、cglib动态代理。

二、静态代理

静态代理,需要代理对象和目标对象实现同一个接口。

现在市面上的游戏很多,我们创建一个接口,写上现在一直很火的一款游戏,英雄联盟


public interface Play {
    public void lol();
}

有一个对象,他叫 “有趣的灵魂200斤”,是个肥宅,名如其人对吧,他有一个爱好,就是玩、娱乐,我们让肥宅小兄弟实现一下这个接口,看看他是怎么玩这个游戏的。


public class FeiZhai implements Play {

    public void lol() {
        System.out.println("B键已扣,不死不休!");
    }
}

有一天我们的肥宅小兄弟,刚开了新的一局英雄联盟排位赛,突然肚子一阵作响,他看了看时间,于是拿起手机打开饿不饿APP订了个餐,然后继续游戏。
那么我们的肥宅小兄弟为什么在网上订餐呢,因为他不会做饭,所以只能在网上订餐,而且商家做好餐,由我们的外卖小哥送过来,就可以吃了,很方便,主要是不耽误玩游戏。


/**
 * 饿不饿App-肥宅小兄弟
 */
public class EbueProxy implements Play {

    //声明目标对象-为肥宅小兄弟服务
    private FeiZhai feiZhai;

    //通过构造方法,获取目标对象,进行赋值
    public EbueProxy(Play play) {
        this.feiZhai = (FeiZhai) play;
    }

    //对原来的方法进行扩展
    public void lol() {
        System.out.println("商家接到订单,开始做饭");
        System.out.println("饭做完了,外卖小哥取到餐");

        //调用目标对象的方法
    //在不影响肥宅小兄弟玩LOL的情况下,进行功能的拓展和增强。
        feiZhai.lol();
        System.out.println("外卖小哥进行配送");
        System.out.println("叮咚!您好,您的餐,请慢用!");
    }
}

我们的肥宅小兄弟,为了不耽误自己玩游戏,委托饿不饿APP送餐上门,这就是一种代理模式,让我们运行看一下结果。


public class main {

    public static void main(String[] args) {

        Play feiZhai = new FeiZhai();

        EbueProxy ebueProxy = new EbueProxy(feiZhai);

        ebueProxy.lol();
    }
}


商家接到订单,开始做饭
饭做完了,外卖小哥取到餐
B键已扣,不死不休!
外卖小哥进行配送
叮咚!您好,您的餐,请慢用!

三、jdk 动态代理

又是充满希望的一天,今天我们的肥宅小兄弟不想玩LOL了,他熟练的拿起手机,打开和平精英,准备吃(kuai)鸡(di)之旅。

我们给接口类加一个方法


public interface Play {
    public void lol();

    public void chiJi();
}

再具体实现一下


public class FeiZhai implements Play {

    public void lol() {
        System.out.println("B键已扣,不死不休!");
    }

    public void chiJi() {
        System.out.println("人体描边说的就是在下了!");
    }
}

到饭点了,又是和昨天一样的情况,我们再写一套代码,委托饿不饿APP帮我们送下餐


/**
 * 饿不饿App
 */
public class EbueProxy implements Play {

    //声明目标对象-为肥宅小兄弟服务
    private FeiZhai feiZhai;

    //通过构造方法,获取目标对象,进行赋值
    public EbueProxy(Play play){
        this.feiZhai = (FeiZhai) play;
    }

    //对原来的方法进行扩展
    public void lol() {
        System.out.println("商家接到订单,开始做饭");
        System.out.println("饭做完了,外卖小哥取到餐");

    //调用目标对象的方法
    //在不影响肥宅小兄弟玩LOL的情况下,进行功能的拓展和增强。
        feiZhai.lol();
        System.out.println("外卖小哥进行配送");
        System.out.println("叮咚!您好,您的餐,请慢用!");
    }

    //对原来的方法进行扩展
    public void chiJi() {
        System.out.println("商家接到订单,开始做饭");
        System.out.println("饭做完了,外卖小哥取到餐");

    //调用目标对象的方法
    //在不影响肥宅小兄弟玩吃鸡的情况下,进行功能的拓展和增强。
        feiZhai.chiJi();
        System.out.println("外卖小哥进行配送");
        System.out.println("叮咚!您好,您的餐,请慢用!");
    }
}

看到这里的小伙伴们,应该都发现问题了,如果肥宅小兄弟每天换个游戏,那我们岂不是要累死了,而且现在还只是1个肥宅小兄弟,如果是2个、3个、100个呢,难道我们要创建不同的 EbueProxy 代理类么?

要知道现在咱们代码中的 EbueProxy 类,只为 FeiZhai 这个类提供代理服务,相当于定制版,如果我想为FeiZhai1FeiZhai2提供代理服务,我还要再写2个代理类,如果这个时候Play接口新加了几个方法,我们又要去维护实现了Play接口的这些类,很不方便。

本着最少的代码干最多的事这种原则,我们不能接受这样的结果。

这个时候我们就要使用 jdk动态代理 了,我们创建一个类 EbueDynamicProxy,实现 InvocationHandler 接口,重写 invoke 方法。


/**
 * 饿不饿App-动态代理
 */
public class EbueDynamicProxy implements InvocationHandler {

    //注意这里,我们换成了Object,意味着我们可以接受不同的目标对象
    private Object object;

    //通过构造方法,获取目标对象,进行赋值
    public EbueDynamicProxy(Object object){
        this.object = object;
    }

    /**
     *
     * 每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,
     * 当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。
     * InvocationHandler这个接口的唯一一个方法 invoke 方法:
     * 该方法接收3个参数:
     * proxy:  指代我们所代理的那个目标对象
     * method: 指代的是我们所要调用目标对象的某个方法的Method对象
     * args:  指代的是调用目标对象某个方法时接受的参数
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("商家接到订单,开始做饭");
        System.out.println("饭做完了,外卖小哥取到餐");

        Object obj = method.invoke(object, args);

        System.out.println("外卖小哥进行配送");
        System.out.println("叮咚!您好,肥先生的餐,请慢用!\n");
        return obj;
    }
}

静态代理和jdk动态代理最大的区别就是,jdk动态代理是jvm帮我们生成了代理类。

至于jvm怎么生成的我们目前不需要去操心,我们看看我们怎么触发去让jvm生成。


public class main {

    public static void main(String[] args) {

        //创建一个目标对象的实例
        Play feizhai = new FeiZhai();

        //创建一个与目标对象相关联的InvocationHandler
        InvocationHandler ebueDynamicProxy = new EbueDynamicProxy(feizhai);

        //获取目标对象的类加载器
        ClassLoader classLoader = feizhai.getClass().getClassLoader();

        //获取目标对象的所有接口
        Class[] interfaces = feizhai.getClass().getInterfaces();

        //Proxy.newProxyInstance:返回代理类的一个实例,返回后的代理类可以当作被代理类使用
        Play feiZhaiProxy = (Play) Proxy.newProxyInstance(classLoader, interfaces, ebueDynamicProxy);

        //英雄联盟
        feiZhaiProxy.lol();

        //吃鸡
        feiZhaiProxy.chiJi();

    }
}

运行一下看看输出


商家接到订单,开始做饭
饭做完了,外卖小哥取到餐
B键已扣,不死不休!
外卖小哥进行配送
叮咚!您好,您的餐,请慢用!

商家接到订单,开始做饭
饭做完了,外卖小哥取到餐
人体描边说的就是在下了!
外卖小哥进行配送
叮咚!您好,您的餐,请慢用!

是不是很简单,接下来我们分析一下原理,知道怎么做还不够,还得知道为什么能这么做。

通过静态代理,我们很明显可以看到,动态代理是由静态代理演化而来,之前我们手写代理类,有几个目标对象就写几个代理类,目标对象里有几个方法,代理类里就写几个方法,现在换成jdk动态代理,我们就靠jvm来动态生成代理类,我们只要需要实现 InvocationHandler 接口,重写 invoke 方法,添加上需要扩展的功能代码,然后交给jvm就可以了,jvm会帮我们在程序运行时生成代理类,代理类的名称为$Proxy0、$Proxy1、$Proxy2 …以此类推。

这个时候我们想看看jvm生成的代理类是什么样的怎么办?我们在main方法里面加上这样一行代码,这样jvm生成的代理类就会出现在项目里的com.sun.proxy这个目录下面了。


public class main {

    public static void main(String[] args) {

        //这行代码的意思是将JDK动态代理生成的class文件保存到本地
        //作者本地jdk1.8亲测可用,有人说新版本的jdk用下面一行好使,不知道是多新的版本,如果有人用了发现没生成,可以换下面这行代码试试!
        //System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        //创建一个目标对象的实例
        Play feizhai = new FeiZhai();

        //创建一个与目标对象相关联的InvocationHandler
        InvocationHandler ebueDynamicProxy = new EbueDynamicProxy(feizhai);

        //获取目标对象的类加载器
        ClassLoader classLoader = feizhai.getClass().getClassLoader();

        //获取目标对象的所有接口
        Class[] interfaces = feizhai.getClass().getInterfaces();

        //Proxy.newProxyInstance:返回代理类的一个实例,返回后的代理类可以当作被代理类使用
        Play feiZhaiProxy = (Play) Proxy.newProxyInstance(classLoader, interfaces, ebueDynamicProxy);

        //英雄联盟
        feiZhaiProxy.lol();

        //吃鸡
        feiZhaiProxy.chiJi();

    }
}

我们运行一下,果然生成了,文件名也是$Proxy开头

代理类

我们再看看class文件里面的代码是什么样的


package com.sun.proxy;

import com.test.proxy.Play;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Play {
    private static Method m1;
    private static Method m2;
    private static Method m0;
    private static Method m3;
    private static Method m4;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void chiJi() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void lol() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.test.proxy.Play").getMethod("chiJi");
            m4 = Class.forName("com.test.proxy.Play").getMethod("lol");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

我们看到在这个类里面,写了一个静态代码块,这里的m1、m2、m3等等,都是使用了反射找到了对应的Method对象。


static {
    try {
        m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        m3 = Class.forName("com.test.proxy.Play").getMethod("lol");
        m4 = Class.forName("com.test.proxy.Play").getMethod("chiJi");
    } catch (NoSuchMethodException var2) {
        throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
        throw new NoClassDefFoundError(var3.getMessage());
    }
}

然后我们再找到我们写的lol方法,我们拿这个进行举例说明。


public final void lol() throws  {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

可以看到代码很简单,主要就是这么一行代码
super.h.invoke(this, m3, (Object[])null);
这行代码的意思就是调用当前类的父类的h变量的invoke方法。这么读发现有点绕是不是,没事,我们来剖析一下看看当前类的父类是谁?当前类的父类的h变量又是什么?调用这个invoke方法又有什么用?来上代码。


import com.test.proxy.Play;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Play  

从上面这段代码我们可以看到,$Proxy0代理类继承了Proxy类,并且实现了我们写的Play接口,这下明白了,当前类的父类就是Proxy类。


public class Proxy implements java.io.Serializable {

    private static final long serialVersionUID = -2222568056686623797L;

    private static final Class<?>[] constructorParams = { InvocationHandler.class };

    private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

    //看这里
    protected InvocationHandler h;

}

再看上面这段代码,h变量就是InvocationHandler类,那么super.h.invoke方法,就相当于是调用了proxy的h变量的invoke方法,还记得invoke方法是什么吗,就是我们写的EbueDynamicProxy类里面的invoke方法,执行的就是这个方法。至于什么时候把EbueDynamicProxy类赋值给了h变量,我们在main方法里不是写了Proxy.newProxyInstance了,就是在那个时候,把EbueDynamicProxy传过去的。


接下来我们来粗略看一下cglib动态代理。

如果你使用了Spring框架,就不用单独引入cglib了,因为Spring集成了cglib。

接下来我们创建一个普通的类,写上一个方法。


public class HelloFeiZhai {

    public void hello(){
        System.out.println("你好,肥宅!");
    }

}

然后我们开始为这个类创建动态代理,首先要创建一个类,实现MethodInterceptor接口,并且重写intercept方法。


public class CglibDynamicProxy implements MethodInterceptor {

    /**
     * 增强代码
     * @param object        被代理的对象
     * @param method        代理的方法
     * @param objects       方法参数
     * @param methodProxy   cglib方法代理对象
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        System.out.println("你好,我叫 有趣的灵魂200斤");

        Object obj = methodProxy.invokeSuper(object, objects);

        System.out.println("我好你******");

        return obj;
    }
}

然后看看main方法


public static void main(String[] args) {

    CglibDynamicProxy cglibDynamicProxy = new CglibDynamicProxy();

    //Enhancer是cglib的字节码增强器,用来创建动态代理类
    Enhancer enhancer = new Enhancer();

    //指定要代理的业务类(即:为要生成的代理类指定父类)
    enhancer.setSuperclass(HelloFeiZhai.class);

    //设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现MethodInterceptor接口的intercept()方法进行拦截
    enhancer.setCallback(cglibDynamicProxy);

    // 创建动态代理类对象并返回
    HelloFeiZhai helloFeiZhai = (HelloFeiZhai)enhancer.create();

    // 调用
    helloFeiZhai.hello();

}

运行结果:


你好,我叫 有趣的灵魂200斤
你好,肥宅!
我好你*****

四、cglib 动态代理

cglib的源码确实比较复杂,我自己还有好多地方不明白,就不拿出来误导大家了,我粗略的说一下原理:通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

五、总结

jdk动态代理在老的java版本中,效率不尽人意,是比cglib要慢的,但是在jdk1.8版本,据大佬说,jdk动态代理已经是比cglib要快了。

在这篇文章中我们讲了java设计模式之一的《代理模式》,介绍了现在java中的三种代理模式(静态代理、jdk动态代理、cglib动态代理),而且分别演示了三种代理模式的使用方法、说明了它们之间的关系和区别,以及优缺点。

如果写的有什么问题,欢迎大家在底下评论指正,我会及时采纳和修改,感谢!

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

推荐阅读更多精彩内容