当前位置: 首页 > 后端技术 > Java

彻底理解代理模式(Proxy)

时间:2023-04-01 13:25:16 Java

代理模式介绍代理模式定义及特点代理模式结构模式实现静态代理[](#)动态代理总结及装饰器模式一文已收录在我的仓库:Java学习笔记和免费图书分享代理模式介绍代理模式是一种很常见的模式,生活中的例子很多。例如,如果你不好意思向你不亲近的朋友求情,你需要找一个和他关系好的人。回应朋友帮忙转达,这位中间朋友就是代理。例如,您不必去火车站买火车票。可通过12306网站或火车票代理处购买。再比如找女朋友、找保姆、找工作等等,都可以通过找中介来完成。代理模式的定义和特点代理模式的定义:由于某些原因,需要为一个对象提供一个代理来控制对该对象的访问。这时,访问对象不适合或不能直接引用目标对象,代理对象充当访问对象和目标对象之间的中介。考虑一个生活中常见的例子。客户想买房子。房东有很多房子,提供卖房服务,但房东不会给客户看房子,所以客户通过中介买房。你可能不明白这里的中介究竟是代客买房还是代房东卖房。其实这个很好理解。我们编写代码为客户服务,中间商代表服务提供商处理业务。这种服务可能定义为卖房,也可能定义为帮助客户买房,但中介是不可能实现买房功能的。在代码中,我们定义的是为客户服务的业务接口,而不是客户的需求接口。如果让客户和中介都实现买房的接口,那么这里的买房就是一种服务于卖房客户的业务。这样一来,房东就是客户端,买房的一方就是服务端。但是在生活中,往往买房的是客户,卖房的是服务员。所以中介和房东必须实现卖房的接口方法。也就是说,中介是卖房子而不是替委托人买房子。客户把中介抽象为房东,直接从中介那里买房(中介==房东,提供卖房服务)。这里中介是代理对象,客户是接入对象,房东是目标对象。实际上,代理完全控制了对目标对象的访问,访问对象的客户端只与代理对象通信。一、代理模式的结构代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面分析其基本结构.代理模式的主要作用如下。抽象主体(Subject)类(业务接口类):通过接口或抽象类声明真实主体和代理对象实现的业务方法,服务端需要实现该方法。RealSubject类(业务实现类):实现抽象Subject中的具体业务,是代理对象所代表的真实对象,是最终被引用的对象。代理(Proxy)类:提供与真实主题相同的接口,其中包含对真实主题的引用,可以访问、控制或扩展真实主题的功能。其结构如图1所示。图1代理模式结构图在代码中,一般将代理理解为代码增强。其实就是在原来的代码逻辑前后加了一些代码逻辑,让调用者没有感知。模式实现根据代理的创建周期,代理模式分为静态代理和动态代理。静态:程序员创建代理类或特定工具自动生成源代码,然后进行编译。代理类的.class文件在程序运行前已经存在。动态:程序运行时,利用反射机制动态创建。静态代理静态代理服务于单一接口。让我们考虑一个实际项目中的示例。现在已经有实现增删改查功能的业务代码了。原来的业务代码还有大量的程序不能改。现在有一个新的需求,就是每执行一个方法输出一条日志。我们不改变原来的代码,而是增加一个代理来实现://业务接口接口DateService{voidadd();voiddel();}classDateServiceImplAimplementsDateService{@Overridepublicvoidadd(){System.out.println("添加成功!");}@Overridepublicvoiddel(){System.out.println("删除成功!");}}类DateServiceProxy实现DateService{DateServiceImplAserver=newDateServiceImplA();@Overridepublicvoidadd(){server.add();System.out.println("程序执行add方法并记录日志。");}@Overridepublicvoiddel(){server.del();System.out.println("程序执行del方法,log。");}}//clientpublicclassTest{publicstaticvoidmain(String[]args){DateServiceservice=newDateServiceProxy();服务.add();服务.del();}}现在,我们已经在不改变程序原有代码的情况下,成功地扩展了一些功能!让我们考虑一下这种情况。当原来的业务处理因为某些原因无法改变,而目前需要扩展一些功能时,可以通过代理的方式来实现:如上图所示,我们原来的业务非常庞大。很难修改,因为牵一发而动全身,但是现在需要扩展一些功能,这里需要实现代理模式,在垂直代码之间,横向扩展一些功能,也叫面向切面编程。如果你有好的设计思路,很快就能发现上面代码的不足之处:一个代理只能服务于具体的业务实现类,假设我们还有另外一个类也实现了业务接口,即类DateServiceImplB实现了DateService,发现如果要扩展这个类,还必须为它写一个代理,可扩展性极低。解决这个问题也很简单。我们是面向接口编程而不是面向实现。我们持有代理类的接口,而不是特定的类:publicDateServiceProxy(DateServiceserver){this.server=server;}}这样的代理可以同时代理多个实现相同业务接口的服务,但是这种方式必须要求客户端传入具体的实现类,这样客户端就必须获取到具体的目标对象比如目标对象直接暴露给访问对象,这在某些情况下是不可接受的,比如你想获取某个资源,但是你需要一定的权限,那么代理控制你对目标资源对象的访问,你不能直接访问,代理必须将目标资源对象牢牢控制在自己手中。这实际上是一个保护代理,后面会提到。但是在这里,这种方法是可以接受的,并且给程序带来了高度的灵活性。动态代理为什么需要动态代理?要理解这一点,我们必须知道静态代理有什么问题。要实现静态代理,就必须事先在程序中硬编码代理类。这是固定的。上面说了,有一些代理类,代理必须要负责一个类。在这种情况下,代理类的数量可能会非常多,但是我们真的会用到每一个代理吗?比如在普通的项目中,99%的时候可能只是简单的查询,不会设计增删改查功能。这时候我们的增删代理类就不需要了,但是在静态代理中,我们还是要硬编码代理类,造成了不必要的资源浪费,增加了代码量。动态代理可以帮助我们只在需要的时候创建代理类,减少资源浪费。另外,由于动态代理是模板形式,还可以减少程序代码量。比如静态代码示例中,我们在每个方法中添加System.out.println("程序执行***方法,记录日志。");,当业务方法比较多时,我们也要添加一个日志每个业务方法的语句,并且在动态代理管理中统一方法,不管几种业务方法,只用一条记录语句就可以实现,具体请参考代码。动态代理使用反射机制在运行时创建接口类的实例。在JDK的实现中,我们需要用到Proxy类和InvocationHandler接口类。运行时动态创建接口实例的方法如下:定义一个类来实现InvocationHandler接口。该接口下有一个invoke(Objectproxy,Methodmethod,Object[]args)方法,负责调用对应接口的接口方法;调用当使用到代理类的方法时,handler会使用反射将代理类、代理类的方法、调用代理类的参数传到这个函数中,并运行这个函数。这个函数其实是在运行的,我们这里写代理。核心代码。通过Proxy.newProxyInstance()创建接口实例,需要3个参数:使用的ClassLoader,通常是接口类的ClassLoader;待实现的接口数组,至少需要传入一个接口;处理程序接口。该方法返回一个代理类$Proxy0,它有三个参数,第一个通常是类本身的ClassLoader,第二个是类要实现的接口,比如这里我们要实现增删改接口,第三个是一个handler接口,也就是当调用这个类的方法时,会将这个类的方法委托给handler,由handler做一些处理。这个对应上面的方法,一般都是设置成这个的。将返回的对象转换为接口。下面看一下具体实现:importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;importjava.lang.reflect.Proxy;//业务接口interfaceDateService{voidadd();无效删除();}classDateServiceImplAimplementsDateService{@Overridepublicvoidadd(){System.out.println("添加成功!");}@Overridepublicvoiddel(){System.out.println("删除成功!");}}类ProxyInvocationHandler实现InvocationHandler{privateDateService服务;publicProxyInvocationHandler(DateServiceservice){this.service=service;}publicObjectgetDateServiceProxy(){returnProxy.newProxyInstance(this.getClass().getClassLoader(),service.getClass().getInterfaces(),this);}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{varresult=method.invoke(service,args);//让服务调用方法,方法返回值System.out.println(proxy.getClass().getName()+"代理类执行"+method.getName()+"方法,返回"+结果+",记录!");返回结果;}}//clientpublicclassTest{publicstaticvoidmain(String[]args){DateServiceserviceA=newDateServiceImplA();DateServiceserviceProxy=(DateService)newProxyInvocationHandler(serviceA).getDateServiceProxy();serviceProxy.add();serviceProxy.del();}}/*添加成功!$Proxy0代理类执行add方法,返回null,记录日志!成功删除!$Proxy0代理类执行del方法,返回null,记录日志!*/我们的代理类是通过Proxy.newProxyInstance(this.getClass().getClassLoader(),service.getClass().getInterfaces(),this)获得的;方法。在这个方法中,我们传入了第二个参数服务类的接口部分,即DateService,通过底层接口的字节码帮我们新建了一个类$Proxy0。这个类有接口的所有方法。第三个参数是处理程序接口,在这里传入。表示该方法交给ProxyInvocationHandler的接口,即InvocationHandler的invoke方法执行。$Proxy没有真正处理它的能力。当我们调用$$Proxy0.add()时,会落入invokehandler,这是我们写核心代码的地方,其中varresult=method.invoke(service,args);调用目标对象的方法,我们可以编写代理的核心代码。我们还可以写一个更通用的接口,可以扩展不同的业务接口。在静态代理中,如果我们要扩展两个接口,至少需要写两个代理类,虽然这两个代理类的代码是一样的,通过一个向上的改造,动态代理可以更好的实现这个功能,并且可以大大减少代码量。importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;importjava.lang.reflect.Proxy;//业务接口接口DateService{voidadd();voiddel();}classDateServiceImplAimplementsDateService{@Overridepublicvoidadd(){System.out.println("成功添加!");}@Overridepublicvoiddel(){System.out.println("成功删除!");}}interfaceOperateService{voidplus();voidsubtract();}classOperateServiceImplAimplementsOperateService{@Overridepublicvoidplus(){System.out.println("+操作");}@Overridepublicvoidsubtract(){System.out.println("-操作");}}//万能的模板classProxyInvocationHandlerimplementsInvocationHandler{privateObjectservice;publicProxyInvocationHandler(Objectservice){this.service=service;}publicObjectgetDateServiceProxy(){returnProxy.newProxyInstance(this.getClass().getClassLoader(),service.getClass().getIn接口(),这个);}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{varresult=method.invoke(service,args);//方法返回值System.out.println(proxy.getClass().getName()+"代理类执行"+method.getName()+"方法,返回"+result+",log!");返回结果;}}//clientpublicclassTest{publicstaticvoidmain(String[]args){DateServicedateServiceA=newDateServiceImplA();DateServicedateServiceProxy=(DateService)newProxyInvocationHandler(dateServiceA).getDateServiceProxy();dateServiceProxy.add();dateServiceProxy.del();OperateServiceoperateServiceA=newOperateServiceImplA();OperateServiceoperateServiceProxy=(OperateService)newProxyInvocationHandler(operateServiceA).getDateServiceProxy();operateServiceProxy.plus();operateServiceProxy.subtract();}}/*添加成功!$Proxy0代理类执行add方法,返回null,记录日志!成功删除!$Proxy0代理类执行del方法,返回null,log!+操作$Proxy1代理类执行plus方法,返回null,记录日志!-操作$Proxy1代理类执行subtract方法,返回null,记录日志!*/Summary代理模式通常有以下用途:远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,当用户申请一些网络磁盘空间时,在文件系统的文件系统中创建一个虚拟硬盘。当用户访问虚拟硬盘时,实际上访问的是网络磁盘空间。虚拟代理,这种方法通常在要创建的目标对象开销很大的时候使用。例如,下载一张大图片需要很长时间,而且由于某些计算的复杂性,无法在短时间内完成。这时候可以先用一小部分虚拟代理代替实物,消除用户服务器慢的感觉。当访问目标对象需要一定的权限时,保护代理提供对目标对象的受控保护,例如,它可以拒绝为权限不足的客户端提供服务。智能引导主要用于在调用目标对象时,为代理添加一些额外的处理功能。比如添加统计一个真实对象被引用次数的功能,这样当对象没有被引用时,可以自动释放(C++智能指针);代理模式的主要优点是:代理模式充当客户端和目标对象之间的中介,保护目标对象;代理对象可以扩展目标对象的功能;代理模式可以将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性;它的主要缺点是:静态代理模式会增加系统设计中类的数量,而动态代理可以解决这个问题;在客户端和目标对象之间加入代理对象会减慢请求处理速度;增加系统的复杂性;我们实现的代理模式和装饰器模式与装饰器模式非常相似,但是它们的目的不同。正如我们上面提到的,有些代理会将访问对象与受控对象严格分开。一个代理只负责一个类,这与装饰器模式不同。对于装饰器模式,目标对象是访问对象所持有的内容。此外,虚拟代理的实现与装饰器模式的实现不同。虚拟代理一开始并没有持有远程服务器的资源对象,而是通过解析域名和文件名来获取对象,这和我们上面的代码是一样的。不同的是,在我们的代码中要么传入一个实例,要么让代理持有一个实例,但是在虚拟代理中,我们传入一个虚拟的文件资源,虚拟代理解析远程服务器得到真实的对象实例,这也是不同的。