设计模式详解之代理模式

代理模式是我们使用率比较高的一个模式。它的定义是为其他对象提供一种代理以控制对这个对象的访问。

如果只是从定义上来看,可能无法理解。为什么要用代理来对这个对象来访问,我直接访问不行吗?行,当然行了。但是我们使用代理自然是有代理的优势,我们举个简单例子来说明一下。

有一个房东,他有一座房子要出售。但是房子买卖呢,我们知道,需要让买房的人来看房,有意向之后还需要和顾客签订一系列的合同,实在复杂。我们的房东比较懒,他不想做那么多事情,他只想单纯卖房子。于是他找了一个中介,由中介来代替房东做这些事情。这个中介就可以说是我们的代理。如果觉得还是觉得抽象的话,下面我们会用实际开发代码来演示代理模式,深入理解。

代理模式的三个重要角色:

Subject抽象主题角色

  • 抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。

RealSubject具体主题角色

  • 被委托角色,被代理角色。也就是我们的房东

Proxy代理主题角色

  • 也叫做委托类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作。(我们的中介)

关于我们的代理模式大致可以分成两种,静态代理模式和动态代理模式。下面我们会用代码来分别演示一下两种代理的区别。

静态代理

静态代理呢,就是我们的每一个具体主题角色都有自己专门的代理角色。我们用代码来说吧,我们开发业务的时候,需要抽象service接口和具体实现。我们模拟一个增删改查。

package proxydemo;

public interface Service {
    
    //比如我们的业务有这四个方法
    
    void add();
    void delete();
    void update();
    void query();
    
}

然后就是我们去实现这个接口

package proxydemo;

public class ServiceImpl implements Service {
    @Override
    public void add() {
        System.out.println("增加操作");
    }

    @Override
    public void delete() {
        System.out.println("删除操作");
    }

    @Override
    public void update() {
        System.out.println("更新操作");
    }

    @Override
    public void query() {
        System.out.println("查询操作");
    }
}

这个service接口就可以看作是我们的抽象主题角色,具体实现类就是我们的具体主题角色。我们可以在客户端使用

package proxydemo;

public class Client {
    public static void main(String[] args) {
        ServiceImpl service = new ServiceImpl();
        service.add();
        service.delete();
        service.update();
        service.query();
    }
}

|----------控制台输出--------|
增加操作
删除操作
更新操作
查询操作

Process finished with exit code 0 

但是如果这个时候来了一个需求,说要在我们使用方法后,日志能够记录下来是进行了什么操作。那我们容易想到的就是在我们的实现类去修改,在使用后方法后用日志记录下来。但是我们在开发中最忌讳就是修改原有的代码,因为我们是要符合开闭原则的。所有我们就可以用代理模式来

package proxydemo;

//我们的代理角色,在不修改原来的代码情况下,扩展了日志的功能
public class ProxyServiceImpl implements Service{

    //使用组合模式代替继承
    private ServiceImpl service;

    public void setService(ServiceImpl service) {
        this.service = service;
    }

    @Override
    public void add() {
        System.out.println("日志输出:使用add方法");
        service.add();
    }

    @Override
    public void delete() {
        System.out.println("日志输出:使用delete方法");
        service.delete();
    }

    @Override
    public void update() {
        System.out.println("日志输出:使用update方法");
        service.update();
    }

    @Override
    public void query() {
        System.out.println("日志输出:使用query方法");
        service.query();
    }
}

然后在我们的客户端使用的时候,只需要给代理角色设置需要被代理的角色就行

package proxydemo;

public class Client {
    public static void main(String[] args) {
        //实例化我们原来的功能
        ServiceImpl service = new ServiceImpl();
        //实例我们的代理角色
        ProxyServiceImpl proxyService = new ProxyServiceImpl();
        //设置需要被代理的角色
        proxyService.setService(service);
        //使用有日志功能的代理方法
        proxyService.add();
        proxyService.delete();
        proxyService.update();
        proxyService.query();
    }
}



|----------控制台输出--------|
日志输出:使用add方法
增加操作
日志输出:使用delete方法
删除操作
日志输出:使用update方法
更新操作
日志输出:使用query方法
查询操作

Process finished with exit code 0

静态代理总结

好处:

  • 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
  • 一些非核心的功能交给了代理角色,实现了业务的分工
  • 公共业务扩展的时候,方便集中管理

缺点:

  • 上面也说了,每一个真实角色就会产生一个代理角色,代码量翻倍

动态代理

动态代理就是为了解决我们上面那个代码量大的解决问题,也是我们真正在开发中会去使用的代理模式。我们的动态代理有两种方式去实现,一个是JDK动态代理(面向接口),一个是CGlib动态代理(面向类)。我们这里主要是介绍概念,所以就用JDK动态代理来实现我们上面的例子,另一种的使用可以看这里(动态代理的两种方式以及区别

(补充:现在也出了Javassit去实现动态代理)

关于JDK的动态代理,我们在java.lang.reflect包下,有这么一个接口

Interface InvocationHandler

InvocationHandler是由代理实例的调用处理程序实现的接口

每个代理实例都有一个关联的调用处理程序。 当在代理实例上调用方法时,方法调用将被编码并分派到其调用处理程序的invoke方法。

还有一个Proxy类,它提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。

可以通过反射来创建代理

  Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                                          new Class<?>[] { Foo.class },
                                          handler); 

话不多说,我们用代码了实现一下更容易明白这些抽象的解释。

例子还是调用我们的上面的例子,只不过这个时候,我们的代理类不需要去实现了,我们实现InvocationHandler接口写调用程序就行了。

package proxydemo;

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

public class ProxyInvocationHandler implements InvocationHandler {

    //被代理的接口
    public Object target;

    public void setTarget(Object object) {
        this.target = object;
    }


    //通过反射生成得到代理类
    public Object getProxy(){
        //三个参数,当前的类加载器,被代理的接口,以及一个InvocationHandler,当前即可
        return Proxy.newProxyInstance
                (this.getClass().getClassLoader(),
                        target.getClass().getInterfaces(),this);
    }

    //处理代理实例,返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通过反射来调用方法
        Object result = method.invoke(target,args);
        return result;
    }
}

我们在我们的客户端使用的时候,需要去实例化一个InvocationHandler就行了。

package proxydemo;

public class Client {
    public static void main(String[] args) {
        //实例一个我们的需要被代理的角色
        ServiceImpl service = new ServiceImpl();
        //实例我们的代理调用处理
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        //设置被代理的角色
        pih.setTarget(service);
        //通过反射了来实例我们的代理类
        Service proxyService = (Service) pih.getProxy();
        proxyService.add();
        proxyService.delete();
        proxyService.update();
        proxyService.query();
    }
}

|----------控制台输出--------|
增加操作
删除操作
更新操作
查询操作

Process finished with exit code 0

具体的调用过程就是

这个时候如果我们要增加一个日志输出功能的话,只需要在我们的ProxyInvocationHandler里面增加就行。

package proxydemo;

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

public class ProxyInvocationHandler implements InvocationHandler {

    //被代理的接口
    private Object target;

    //设计被代理的接口
    public void setTarget(Object object) {
        this.target = object;
    }

    //通过反射生成得到代理类
    public Object getProxy(){
        //三个参数,当前的类加载器,被代理的接口,以及一个InvocationHandler,当前即可
        return Proxy.newProxyInstance
                (this.getClass().getClassLoader(),
                        target.getClass().getInterfaces(),this);
    }

    //处理代理实例,返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通过反射来调用方法
        log(method.getName());  //将方法名传给我们的输出方法
        Object result = method.invoke(target,args);
        return result;
    }
    
    //新增加的日志输出功能
    public void log(String msg){
        System.out.println("日志输出,调用了"+msg+"方法");
    }
}

客户端输出:

日志输出,调用了add方法
增加操作
日志输出,调用了delete方法
删除操作
日志输出,调用了update方法
更新操作
日志输出,调用了query方法
查询操作

Process finished with exit code 0

看到这里可能还是会有小伙伴不是很懂, 上面我们的静态代理实现了一个代理类,这里不也是实现了一个调用处理吗?注意,这里我们的调用处理可是一个可以复用的,只要你是实现了基于接口实现的业务就可以调用。比如我们一个庞大的业务的层,有很多的servcie接口,都可以统一使用这个动态代理模板。我们不需要在实现阶段去关心代理谁,在使用的时候才指定代理谁!!!

总结

关于代理我们也了解,它主要的优点也很明显

  • 职责清晰。真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件事务,附带的结果就是编程简洁清晰。
  • 高扩展性。具体主题角色是随时都会发生变化的,只要它实现了接口,甭管它如何变化,都逃不脱如来佛的手掌(接口),那我们的代理类完全就可以在不做任何修改的情况下使用。
  • 智能化。都不用自己实现代理,只需要在使用的时候去指定就行了。

使用场景

关于代理模式应用的场景,一个比较典型的动态代理就是Spring AOP了,我们用AOP来面向切面编程。在我们完成核心业务之后,然后可以横向扩展我们的周边业务,比如日志输出,监控等。具体的可以去了解AOP,了解之后对其他的场景,下次一见到就能看出来这用到了代理模式了。

参考资料

设计模式之禅(第二版)

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