当前位置:首页>编程知识库>后端开发知识>设计模式是什么鬼(代理)
设计模式是什么鬼(代理)
阅读 1
2018-12-03

//本文作者:凸凹里歐

//本文收录菜单栏:《设计模式是什么鬼》专栏中

代理,代表打理,以他人的名义代表委托人打理其本职工作之外或不所能及的事务,达成合作关系并更高效地促成事务完成的目的。例如明星经纪人,他们并没有像明星一样会唱歌、跳舞或演戏,而是替明星打理一些无暇顾及的事务(这并不代表可以代理分外之事),比如推广与宣传,合同谈判啊之类,达成和约后他们才会通知明星去表演。再比如机票销售代理商既不造飞机也不提供乘机服务,他们只负责卖票,代理律师并不会因胜诉获得赔偿金或者败诉受到法律制裁,他们只负责代理打官司,等等等等。
图片来源网络,侵删
生活中还有很多其他实例不胜枚举,但对于做技术的我们,以网络代理举例相信是最合适不过了。
首先,我们上网前得去网络服务提供商(ISP)申请互联网宽带业务,于是顺理成章光纤入户,并拿到一个调制解调器,也就是我们俗称的“猫”。好,“猫”实现了互联网访问接口,看代码。
1public%20interface%20Internet%20%7B//%E4%BA%92%E8%81%94%E7%BD%91%E8%AE%BF%E9%97%AE%E6%8E%A5%E5%8F%A3%0A2%0A3%20%20%20%20public%20void%20access(String%20url);%0A4%0A5%7D%0A
1public%20class%20Modem%20implements%20Internet%20%7B//%E8%B0%83%E5%88%B6%E8%A7%A3%E8%B0%83%E5%99%A8%0A2%0A3%20%20%20%20@Override%0A4%20%20%20%20public%20void%20access(String%20url)%7B//%E5%AE%9E%E7%8E%B0%E4%BA%92%E8%81%94%E7%BD%91%E8%AE%BF%E9%97%AE%E6%8E%A5%E5%8F%A3%0A5%20%20%20%20%20%20%20%20System.out.println(%22%E6%AD%A3%E5%9C%A8%E8%AE%BF%E9%97%AE%EF%BC%9A%22%20 %20url);%0A6%20%20%20%20%7D%0A7%7D%0A
作为调制解调器,一定有上网功能了,用户的电脑只需要用网线连接这只“猫”便接入互联网了。就这么简单么?然而某天我们发现孩子学习时总是偷偷上网看电影玩游戏,于是我们决定对某些网站进行过滤,拒绝黄赌毒侵害未成年。那么,我们需要在客户终端电脑与猫之间加一层代理,用于过滤某些不良网站,最终我们决定购买一款有过滤功能的路由器。
%201public%20class%20RouterProxy%20implements%20Internet%20%7B//%E8%B7%AF%E7%94%B1%E5%99%A8%E4%BB%A3%E7%90%86%E7%B1%BB%0A%202%0A%203%20%20%20%20private%20Internet%20modem;//%E6%8C%81%E6%9C%89%E8%A2%AB%E4%BB%A3%E7%90%86%E7%B1%BB%E5%BC%95%E7%94%A8%0A%204%20%20%20%20private%20List<String>%20blackList%20=%20Arrays.asList(%22%E7%94%B5%E5%BD%B1%22,%20%22%E6%B8%B8%E6%88%8F%22,%20%22%E9%9F%B3%E4%B9%90%22,%20%22%E5%B0%8F%E8%AF%B4%22);%0A%205%0A%206%20%20%20%20public%20RouterProxy()%20%7B%0A%207%20%20%20%20%20%20%20%20this.modem%20=%20new%20Modem();//%E5%AE%9E%E4%BE%8B%E5%8C%96%E8%A2%AB%E4%BB%A3%E7%90%86%E7%B1%BB%0A%208%20%20%20%20%20%20%20%20System.out.println(%22%E6%8B%A8%E5%8F%B7%E4%B8%8A%E7%BD%91...%E8%BF%9E%E6%8E%A5%E6%88%90%E5%8A%9F%EF%BC%81%22);%0A%209%20%20%20%20%7D%0A10%0A11%20%20%20%20@Override%0A12%20%20%20%20public%20void%20access(String%20url)%20%7B//%E5%90%8C%E6%A0%B7%E5%AE%9E%E7%8E%B0%E4%BA%92%E8%81%94%E7%BD%91%E8%AE%BF%E9%97%AE%E6%8E%A5%E5%8F%A3%E6%96%B9%E6%B3%95%0A13%20%20%20%20%20%20%20%20for%20(String%20keyword%20:%20blackList)%20%7B//%E5%BE%AA%E7%8E%AF%E9%BB%91%E5%90%8D%E5%8D%95%0A14%20%20%20%20%20%20%20%20%20%20%20%20if%20(url.contains(keyword))%20%7B//%E6%98%AF%E5%90%A6%E5%8C%85%E5%90%AB%E9%BB%91%E5%90%8D%E5%8D%95%E5%AD%97%E7%9C%BC%0A15%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20System.out.println(%22%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9A%22%20 %20url);%0A16%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return;%0A17%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A18%20%20%20%20%20%20%20%20%7D%0A19%20%20%20%20%20%20%20%20modem.access(url);//%E6%AD%A3%E5%B8%B8%E8%AE%BF%E9%97%AE%E4%BA%92%E8%81%94%E7%BD%91%0A20%20%20%20%20%7D%0A21%7D%0A
注意看,在这里路由器代理主要充当代理的角色,和之前的“猫”一样,它同样实现了互联网接口,看似也是有上网功能的,其实不然。第12行代码对于互联网访问功能的实现一开始就做了个过滤,如果地址中带有黑名单中的敏感字眼则禁止访问并直接退出,反之则于第19行调用“猫”的互联网访问方法,看到了吧,最终还是调用“猫”的上网功能。注意此处为了对“猫”进行控制,代理专为此而生,我们直接于第7行实例化它而不是需要别人把它注入进来。好了,孩子现在来上网了,迫不及待运行之。
%201public%20class%20Client%20%7B%0A%202%20%20%20%20public%20static%20void%20main(String%5B%5D%20args)%20%7B%0A%203%20%20%20%20%20%20%20%20Internet%20proxy%20=%20new%20RouterProxy();//%E5%AE%9E%E4%BE%8B%E5%8C%96%E7%9A%84%E6%98%AF%E4%BB%A3%E7%90%86%0A%204%20%20%20%20%20%20%20%20proxy.access(%22http://www.%E7%94%B5%E5%BD%B1.com%22);%0A%205%20%20%20%20%20%20%20%20proxy.access(%22http://www.%E6%B8%B8%E6%88%8F.com%22);%0A%206%20%20%20%20%20%20%20%20proxy.access(%22ftp://www.%E5%AD%A6%E4%B9%A0.com/java%22);%0A%207%20%20%20%20%20%20%20%20proxy.access(%22http://www.%E5%B7%A5%E4%BD%9C.com%22);%0A%208%0A%209%20%20%20%20%20%20%20%20/*%20%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C%0A10%20%20%20%20%20%20%20%20%20%20%20%20%E6%8B%A8%E5%8F%B7%E4%B8%8A%E7%BD%91...%E8%BF%9E%E6%8E%A5%E6%88%90%E5%8A%9F%EF%BC%81%0A11%20%20%20%20%20%20%20%20%20%20%20%20%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E7%94%B5%E5%BD%B1.com%0A12%20%20%20%20%20%20%20%20%20%20%20%20%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E6%B8%B8%E6%88%8F.com%0A13%20%20%20%20%20%20%20%20%20%20%20%20%E6%AD%A3%E5%9C%A8%E8%AE%BF%E9%97%AE%EF%BC%9Aftp://www.%E5%AD%A6%E4%B9%A0.com/java%0A14%20%20%20%20%20%20%20%20%20%20%20%20%E6%AD%A3%E5%9C%A8%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E5%B7%A5%E4%BD%9C.com%0A15%20%20%20%20%20%20%20%20*/%0A16%20%20%20%20%7D%0A17%7D%0A
在第3行处,孩子实例化的不再是“猫”,而是被偷梁换柱的替换为路由器代理了,也就是说大家上网都连接路由器了,而不是直接去连“猫”,这样不但省去了我们拨号的麻烦(路由器帮助拨号)而且孩子再也访问不到乱七八糟的网站了。而这个代理自身其实并不具备访问互联网的能力,它只是简单的调用“猫”上网功能,其存在目的只是为了控制对”猫“的互联访问,对其进行代理而已。
说到这里大家有没有发现这个代理模式是不是与装饰器模式很类似?如果观察UML类图关系你会发现几乎一模一样,那这个模式存在的意义何在?其实,代理模式更强调的是对被代理对象的控制,而不是仅限于去装饰目标对象并增强其原有的功能。就像明星的例子一样,如果钱没给够,合同未达成,则不让明星随意作秀。
相信大家已经理解地很通透了吧,这也是我们最常用的代理模式了。其实还有一种叫动态代理,不同之处在于其实例化过程是在运行时完成的,也就是说我们不需要专门针对某个接口去写这么一个代理类,而是根据接口动态生成。
举个例子,让我们先忘掉之前的路由器代理,当我们内网中的上网设备越来越多,路由器的Lan口已被占满不够用了,于是我们决定换成交换机,看代码。
1public%20interface%20Intranet%20%7B//%E5%B1%80%E5%9F%9F%E7%BD%91%E8%AE%BF%E9%97%AE%E6%8E%A5%E5%8F%A3%0A2%0A3%20%20%20%20public%20void%20fileAccess(String%20path);%0A4%0A5%7D%0A
为了保持简单,我们假设这个交换机Switch实现了局域网访问接口Intranet,请注意这里不是互联网接口Internet
1public%20class%20Switch%20implements%20Intranet%20%7B%0A2%0A3%20%20%20%20@Override%0A4%20%20%20%20public%20void%20fileAccess(String%20path)%7B%0A5%20%20%20%20%20%20%20%20System.out.println(%22%E8%AE%BF%E9%97%AE%E5%86%85%E7%BD%91%EF%BC%9A%22%20 %20path);%0A6%20%20%20%20%7D%0A7%0A8%7D%0A
这里进行的是局域网文件访问,比如说是拷贝另一台内网机器上的共享文件,并且我们想保证与之前一样的关键字过滤控制功能,也就是说不管是什么地址都要先通过过滤,怎么复用呢?
到这里让我们思考一下,猫实现的是互联网访问接口,交换机实现的是局域网访问接口,那我们的过滤器代理类到底该怎么写?是实现Internet接口呢还是实现Intranet接口呢?要么两个都实现?再加进来新的类接口又要不停地改实现类吗?这显然行不通,过滤器无非就是一段过滤逻辑不必来回改动,这违反了设计模式开闭原则。动态代理应时而生,我们来看代码。
%201public%20class%20KeywordFilter%20implements%20InvocationHandler%20%7B%0A%202%0A%203%20%20%20%20private%20List<String>%20blackList%20=%20Arrays.asList(%22%E7%94%B5%E5%BD%B1%22,%20%22%E6%B8%B8%E6%88%8F%22,%20%22%E9%9F%B3%E4%B9%90%22,%20%22%E5%B0%8F%E8%AF%B4%22);%0A%204%0A%205%20%20%20%20//%20%E8%A2%AB%E4%BB%A3%E7%90%86%E7%9A%84%E7%9C%9F%E5%AE%9E%E5%AF%B9%E8%B1%A1,%E7%8C%AB%E3%80%81%E4%BA%A4%E6%8D%A2%E6%9C%BA%E3%80%81%E6%88%96%E6%98%AF%E5%88%AB%E7%9A%84%E4%BB%80%E4%B9%88%E9%83%BD%E6%98%AF%E3%80%82%0A%206%20%20%20%20private%20Object%20origin;%0A%207%0A%208%20%20%20%20public%20KeywordFilter(Object%20origin)%20%7B%0A%209%20%20%20%20%20%20%20%20this.origin%20=%20origin;//%E6%B3%A8%E5%85%A5%E8%A2%AB%E4%BB%A3%E7%90%86%E5%AF%B9%E8%B1%A1%0A10%20%20%20%20%20%20%20%20System.out.println(%22%E5%BC%80%E5%90%AF%E5%85%B3%E9%94%AE%E5%AD%97%E8%BF%87%E6%BB%A4%E6%A8%A1%E5%BC%8F...%22);%0A11%20%20%20%20%7D%0A12%0A13%20%20%20%20@Override%0A14%20%20%20%20public%20Object%20invoke(Object%20proxy,%20Method%20method,%20Object%5B%5D%20args)%20throws%20Throwable%20%7B%0A15%20%20%20%20%20%20%20%20//%E8%A6%81%E8%A2%AB%E5%88%87%E5%85%A5%E6%96%B9%E6%B3%95%E9%9D%A2%E4%B9%8B%E5%89%8D%E7%9A%84%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91%0A16%20%20%20%20%20%20%20%20String%20arg%20=%20args%5B0%5D.toString();%0A17%20%20%20%20%20%20%20%20for%20(String%20keyword%20:%20blackList)%20%7B%0A18%20%20%20%20%20%20%20%20%20%20%20%20if%20(arg.toString().contains(keyword))%20%7B%0A19%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20System.out.println(%22%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9A%22%20 %20arg);%0A20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20null;%0A21%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A22%20%20%20%20%20%20%20%20%7D%0A23%20%20%20%20%20%20%20%20//%E8%B0%83%E7%94%A8%E7%9C%9F%E5%AE%9E%E7%9A%84%E8%A2%AB%E4%BB%A3%E7%90%86%E5%AF%B9%E8%B1%A1%E6%96%B9%E6%B3%95%0A24%20%20%20%20%20%20%20%20return%20method.invoke(origin,%20arg);%0A25%20%20%20%20%7D%0A26%0A27%7D%0A
对于这个关键字过滤功能我们不再写到代理类里面了,而是另外写个类并实现JDK反射包中提供的InvocationHandler接口,于第9行注入即将被代理的对象,不管是猫还是交换机什么的它总归是个Object,然后在第14行实现这个invoke调用方法,之后生成的动态代理将来会调进来跑这块的逻辑,很显然我们这里依然保持不变的逻辑,在真实对象方法被执行之前运行了过滤逻辑加以控制。由于传入的参数是被代理对象的方法method,以及一堆参数args,所以注意这里第24行我们要用反射去调用被代理对象origin了,最后来看我们如何运行。
%201public%20class%20Client%20%7B%0A%202%20%20%20%20public%20static%20void%20main(String%5B%5D%20args)%20%7B%0A%203%0A%204%20%20%20%20%20%20%20%20//%E8%AE%BF%E9%97%AE%E5%A4%96%E7%BD%91%EF%BC%88%E4%BA%92%E8%81%94%E7%BD%91%EF%BC%89,%E7%94%9F%E6%88%90%E7%8C%AB%E4%BB%A3%E7%90%86%E3%80%82%0A%205%20%20%20%20%20%20%20%20Internet%20internet%20=%20(Internet)%20Proxy.newProxyInstance(%0A%206%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Modem.class.getClassLoader(),%0A%207%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Modem.class.getInterfaces(),%20%0A%208%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20new%20KeywordFilter(new%20Modem()));%0A%209%20%20%20%20%20%20%20%20internet.access(%22http://www.%E7%94%B5%E5%BD%B1.com%22);%0A10%20%20%20%20%20%20%20%20internet.access(%22http://www.%E6%B8%B8%E6%88%8F.com%22);%0A11%20%20%20%20%20%20%20%20internet.access(%22http://www.%E5%AD%A6%E4%B9%A0.com%22);%0A12%20%20%20%20%20%20%20%20internet.access(%22http://www.%E5%B7%A5%E4%BD%9C.com%22);%0A13%0A14%20%20%20%20%20%20%20%20//%E8%AE%BF%E9%97%AE%E5%86%85%E7%BD%91%EF%BC%88%E5%B1%80%E5%9F%9F%E7%BD%91%EF%BC%89%EF%BC%8C%E7%94%9F%E6%88%90%E4%BA%A4%E6%8D%A2%E6%9C%BA%E4%BB%A3%E7%90%86%E3%80%82%0A15%20%20%20%20%20%20%20%20Intranet%20intranet%20=%20(Intranet)%20Proxy.newProxyInstance(%0A16%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Switch.class.getClassLoader(),%0A17%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Switch.class.getInterfaces(),%20%0A18%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20new%20KeywordFilter(new%20Switch()));%0A19%20%20%20%20%20%20%20%20intranet.fileAccess(%22%5C%5C%5C%5C192.68.1.2%5C%5C%E5%85%B1%E4%BA%AB%5C%5C%E7%94%B5%E5%BD%B1%5C%5CIronHuman.mp4%22);%0A20%20%20%20%20%20%20%20%20intranet.fileAccess(%22%5C%5C%5C%5C192.68.1.2%5C%5C%E5%85%B1%E4%BA%AB%5C%5C%E6%B8%B8%E6%88%8F%5C%5CHero.exe%22);%0A21%20%20%20%20%20%20%20%20intranet.fileAccess(%22%5C%5C%5C%5C192.68.1.4%5C%5Cshared%5C%5CJava%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99.zip%22);%0A22%20%20%20%20%20%20%20%20intranet.fileAccess(%22%5C%5C%5C%5C192.68.1.6%5C%5CJava%E7%9F%A5%E9%9F%B3%5C%5C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%98%AF%E4%BB%80%E4%B9%88%E9%AC%BC.doc%22);%0A23%0A24%20%20%20%20%20%20%20%20/*%0A25%20%20%20%20%20%20%20%20%20%20%20%20%E5%BC%80%E5%90%AF%E5%85%B3%E9%94%AE%E5%AD%97%E8%BF%87%E6%BB%A4%E6%A8%A1%E5%BC%8F...%0A26%20%20%20%20%20%20%20%20%20%20%20%20%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E7%94%B5%E5%BD%B1.com%0A27%20%20%20%20%20%20%20%20%20%20%20%20%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E6%B8%B8%E6%88%8F.com%0A28%20%20%20%20%20%20%20%20%20%20%20%20%E6%AD%A3%E5%9C%A8%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E5%AD%A6%E4%B9%A0.com%0A29%20%20%20%20%20%20%20%20%20%20%20%20%E6%AD%A3%E5%9C%A8%E8%AE%BF%E9%97%AE%EF%BC%9Ahttp://www.%E5%B7%A5%E4%BD%9C.com%0A30%20%20%20%20%20%20%20%20%20%20%20%20%E5%BC%80%E5%90%AF%E5%85%B3%E9%94%AE%E5%AD%97%E8%BF%87%E6%BB%A4%E6%A8%A1%E5%BC%8F...%0A31%20%20%20%20%20%20%20%20%20%20%20%20%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9A%5C%5C192.68.1.2%5C%E5%85%B1%E4%BA%AB%5C%E7%94%B5%E5%BD%B1%5CIronHuman.mp4%0A32%20%20%20%20%20%20%20%20%20%20%20%20%E7%A6%81%E6%AD%A2%E8%AE%BF%E9%97%AE%EF%BC%9A%5C%5C192.68.1.2%5C%E5%85%B1%E4%BA%AB%5C%E6%B8%B8%E6%88%8F%5CHero.exe%0A33%20%20%20%20%20%20%20%20%20%20%20%20%E8%AE%BF%E9%97%AE%E5%86%85%E7%BD%91%EF%BC%9A%5C%5C192.68.1.4%5Cshared%5CJava%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99.zip%0A34%20%20%20%20%20%20%20%20%20%20%20%20%E8%AE%BF%E9%97%AE%E5%86%85%E7%BD%91%EF%BC%9A%5C%5C192.68.1.6%5CJava%E7%9F%A5%E9%9F%B3%5C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%98%AF%E4%BB%80%E4%B9%88%E9%AC%BC.doc%0A35%0A36%20%20%20%20%20%20%20%20*/%0A37%20%20%20%20%7D%0A38%7D%0A
可以看到,我们不管是访问互联网还是局域网,只需要分别生成相应的代理并调用即可,相同的过滤器逻辑被执行了。如此一来,我们并不需要再写任何的代理类了,只需要实现一次InvocationHandler就一劳永逸了,在运行时去动态地生成代理,达到兼容任何接口的目的。
其实在很多框架中大量应用到了动态代理模式,比如Spring的面向切面AOP,我们只需要定义好一个切面类@Aspect,声明其切入点@Pointcut(被代理的哪些对象的哪些方法,也就是这里的猫和交换机的access以及accessFile),以及被切入的代码块(要增加上去的逻辑,比如这里的过滤功能代码,可分为前置执行@Before,后置执行@After,以及异常处理@AfterThrowing等),于是框架自动帮我们生成代理并切入目标执行。正如给每给方法前后加入日志的例子,或者更经典的事务控制的例子,在所有业务代码之前先切入“事务开始”,执行过后再切入“事务提交”,如果抛异常被捕获则执行“事务回滚”,如此就不必要在每个业务类中去写这些重复代码了,一劳永逸,冗余代码大量减少,开发效率惊人提升。
图片来源网络,侵删
两耳不闻窗外事,一心只读圣贤书,毕竟隔行如隔山,山外之事还是交给专家去代理吧。
点击图片加入Spring交流群
↓↓↓
看完本文有收获?请转发分享给更多人
以上数据来源于网络,如有侵权,请联系删除。
评论 (0)