代理模式(Proxy Pattern)
在现实世界中,很多带有自己产品的工厂都会选择将自己的产品通过代理商的渠道销售到客户手里。它们为什么要这么做呢?个人看来其中一个原因是工厂如果选择直营,客户原先并不知道这个厂的存在,这时工厂就得从建立卖场到推广品牌一整套流程走下来去宣传自己的产品,这其中的投入是非常巨大的,并且前途还未知。另外一个原因是代理商本身就是在某一片地区拥有着属于自己的强大资源,很多客户都知道这个商家,工厂通过他们去做销售,自己也有更多的时间去投入到产品开发和生产,专心做好产品提供服务,两者各司其职,效果更佳。
代理模式也是基于这样的思维,当开发中遇到难以直接访问或者很难访问一个对象的时候就可以优先选择这种模式去处理。就好比工厂通过代理商售卖产品,而如果抛开了代理商,工厂又没有自己的直营渠道,那客户买件商品就得跑到厂家那边,想想是不是觉得成本有点大,并且对客户不是很友好。而通过厂家直接供货到代理商,厂家牺牲一部分的利润,但也能够得到更好的产品销售效果。
Provide a surrogate or placeholder for another object to control access to it.
代理模式就是给某一个对象提供一个代理或者占位符,并由它去控制对原对象的访问。
代理模式的主要参与者
① 抽象主题(Subject) : 为了能让客户端透明化的对代理类和被代理类进行访问。
② 代理类(Proxy) :包含了对被代理类的引用,可以控制对被代理类的引用,同时也可以在调用被代理类操作的之前或者之后执行一些操作。
③ 被代理类(RealSubject) :它提供了被代理的真实业务操作。
典型的代理模式代码如下:
在实际开发中,代理类的实现肯定不可能这么简单,但是整体的思维就是如此。
代理模式的类型
不同的目的和场景孕育出了很多种类的代理模式,较为常见的有远程代理(Remote Proxy),虚拟代理(Virtual Proxy),保护代理(Protect Proxy),缓冲代理(Cache Proxy),智能引用代理(Smart Reference Proxy),下面简要说明其区别。
-
远程代理 :为一个位于不同地址空间的对象提供一个本地的代理对象。这个处于不同地址空间的对象可能在同一个物理空间内,也可能不同。
-
虚拟代理 :相当于一个门面代理。若某个时候需要创建一个开销非常大的对象,此时可以创建一个开销较小的对象来表示,当真正需要时再对真实对象进行创建。
-
保护代理 :控制对一种资源的访问,个人理解为权限代理,可以给不同的用户提供不同级别的使用权限。
-
缓冲代理 :给一个结果提供一个临时的存储空间,以供多个客户端共享这个结果。
-
智能引用代理 :对某项资源进行访问时提供一些额外的、和具体工作无相关性的操作。如对某项资源的访问进行操作记录。
静态代理、保护代理,智能引用代理
举一个例子,现在我们要对一个系统进行请求某项服务。那么,客户端会先去授权中心获取权限证书,再将证书和密文发送给系统,系统服务的代理者会对请求进行权限验证,权限通过则给客户端提供服务。针对此种情况,可以得到如下的类图。
GrantAuthorization类用于给请求提供授权证书和密文,Validator主要用于权限鉴定,RealService为被代理类,主要通过request()方法提供服务,ServiceProxy为代理类。
GrantAuthorization类,提供encrypt()方法对数据进行加密。
Validator类,提供decrypt() 方法对密文进行解密。
Service接口,定义了request()方法。
RealService类,简单的实现了下Service接口。
ServiceProxy类,对RealService进行代理。
客户端:
上述代码其实是静态代理和保护代理的应用实例,如果在这基础上,对用户的访问行为进行一个日志记录,那便多起到了一个智能引用代理的效果。这种设计的好处是,如果某天对于真实服务类的访问方式有了新的调整,则只需新增一个代理类,再通过配置文件的方式将类配置进去,就可以对业务进行修改,无需更改源代码,符合开闭原则。
动态代理(基于java)
在上述静态代理的例子中,通过代理类去代理业务对象,这种方式面向的是对于需求非常确定的,并且接口、接口方法都是事先已经存在,已经定义好的情况。每个代理类都和一个接口以及一组接口方法绑定在一起。同时,在你需要对接口中的多个方法进行相同的代理操作时,你还是不得不对所有被代理方法编写机械化的冗余代码。而且在你需要对不同接口的被代理对象实现代理时,又得相应的增加代理类。这样会导致系统类的数量和无用代码的数量暴增。
动态代理主要是为了解决前一段所描述的问题,它提供了一种在运行时根据需要来动态创建代理类的功能,让同一个代理类能够对多种主题的类进行代理,并且可以代理不同的方法。它在AOP,事务处理,拦截机制等领域起到了非常重要的作用。
从jdk1.3开始,java语言提供了对动态代理的支持,我们在使用jdk提供的动态代理时,主要关注的是java.lang.reflect包下的Proxy类和InvocationHandler接口。这里不谈原码,只谈实现。
抽象接口
被代理类,doSomething() 为被代理方法
动态代理类
getProxy()方法返回一个由Proxy类构造的一个动态代理对象,动态代理类需要实现InvocationHandler接口,并且实现invoke()方法,invoke()方法的三个参数的含义分别为代理对象($proxy),被代理方法,被代理方法参数。JDKDynamicProxy类的target对象其实对应的是被代理对象,将其注入即可。
测试类
运行结果
这里要强调的一点是jdk的动态代理不仅会代理被代理类的方法,还会代理其父类的public方法。就比如测试类里演示的toString().虽然没有显示定义,但是在java中,每种类都继承了Object。新生成的中间代理类都是继承于Proxy类。中间代理类会通过静态载入的方式初始化方法对象,然后对每个方法都进行相应代理,调用方式就是以super.invocationhandler.方法()的形式。具体的代码可以通过修改sun.misc.ProxyGenerator.saveGeneratedFiles参数获取中间代理类。
中间代理类的代码如下(这里使用idea反编译出来的,所以一些参数名没有显现出来,不过并不影响对其的查看和理解):
public final class $Proxy0 extends Proxy implements DynamicService {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
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 void doSomething() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
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);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.adi.proxy_pattern.dynanic_proxy_by_jdk.DynamicService").getMethod("doSomething");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
jdk动态代理基本引用关系如下:
到此处基本上动态代理我们已经能够有个十分感性的认知了,但jdk动态代理的原码里面有许多值得去一看的代码细节。
jdk的动态代理的好处是显然的,但是它也存在着缺点,最大的缺点在于它的代理机制只面向接口,当你想对一个没有公共接口的类进行代理时,你将毫无办法。
这里再多介绍另一种动态代理机制——CGLIB。CGLIB主要就是弥补了jdk无法直接对实现类进行代理的问题,其底层通过ASM操作字节码从而生成新的类,经而达到代理的效果,效率非常高。使用cglib实现动态代理时我们主要关注的是MethodInterceptor接口和Enhancer类。
被代理类
Cglib代理规则
测试类
执行结果:
这里要注意的点是cglib无法对final修饰的方法进行代理,其实想想也无可厚非,因为你已经将这个方法定死了,那也就没有产生代理行为的必要了。这里的动态代理中间类可以通过System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, “输出路径”)获取到。
jdk提供的动态代理和cglib提供的动态代理两者最大的区别是cglib能够对普通类进行代理,而jdk的动态代理只能对接口进行代理。在实现上,jdk的动态代理主要是利用反射的方式调用代理方法达到代理行为,较为高效,而cglib则主要是通过解析字节码构造新类的方式达到代理目的,相比而言,其更为高效。
远程代理
远程代理使得客户端可以访问在远程主机上的对象,它是远程主机在本地的一个代理对象,其将网络通信的细节都给隐藏起来,由它完全负责与远程主机通信,并且调用远程主机上的对应方法,而客户端则完全不用考虑网络的存在。
对于这方面,java也有提供对应的机制——RMI(Remote Method Invocation,远程方法调用),它支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。也就是它可以让一台JVM远程使用另一台JVM上某个对象的方法。
这里有必要先来搞清楚几个概念。
传输层(Transport Layer) 负责管理服务端和客户端间的网络连接。
桩(Stub) 就是服务端服务对象于客户端的分身,也就是一个远程对象的本地代理,它的工作类似网关。
骨干网(Skeleton) 是服务端负责和桩之间的直接沟通对象,由它将请求发放至远程对象。
远程引用层(Remote Reference Layer,RRL) 负责管理客户端和服务端间工作期间产生的引用,也可以理解为客户端和服务端间的虚拟连接。
Transport Layer − This layer connects the client and the server. It manages the existing connection and also sets up new connections.
Stub − A stub is a representation (proxy) of the remote object at
client. It resides in the client system; it acts as a gateway for the
client program.
Skeleton − This is the object which resides on the server side. stub communicates with this skeleton to pass request to the remote object.
RRL(Remote Reference Layer) − It is the layer which manages the
references made by the client to the remote object.
图片来自:https://www.tutorialspoint.com/java_rmi/java_rmi_introduction.htm
RMI的注册机制
承接上文所提到的概念,服务端会在客户端本地发放一个远程代理(Stub,桩),当然服务端在此之前会先将被代理的远程对象先给生成。材料都准备好了,但是它们之间如何有一个映射关系呢?这时就需要注册机制了,服务端将被远程对象实例化后注册到注册中心上,并且指定一个唯一的key。客户端通过指定key得到一个本地代理对象。
图片来自:https://www.tutorialspoint.com/java_rmi/java_rmi_introduction.htm
下面写个简单的小例子。
运行结果
虚拟代理
对于某些占用系统资源较多或者加载时间较长的对象,可以考虑给这些对象加一个虚拟代理。当真实对象创建完毕后,再将请求转发至真实对象。
使用这种代理一般要从三点考虑:
①如果某个对象的创建因网络或者对象本身的复杂性导致创建该真实对象的时间比创建代理对象的时间要大得多。
②机器的资源并不宽裕,对某个耗费资源十分大的对象可以采用此方法。
③ 无关紧要的系统对象,并且这些对象相对于其代理对象实例化的成本大,可以考虑使用该方法。
下面举一个例子。
Picture接口定义对图片的操作。
ConcretePicture模拟一张非常大的图片。
VirtualProxy代理一些有需要被代理的图片。当真正调用download()方法时再通过另一个工作线程去实例化这张大图片。在此之前对图片的显示可以仅限于使用一个占位符或者一张缩略图。
缓冲代理
当需要对某个被频繁访问对象提高响应效率时,可以考虑使用此种方法。通过对热点数据提供一个临时存储空间,方便共享,从而避免一些重复性的操作,进而提高响应效率。
代码演示:
简易的缓存类
模拟数据查询的entity
公共接口
被代理对象
缓冲代理
测试代码
测试结果
通过ProductDaoProxy类对ProductDaoImpl类的getAllProducts()进行代理,将其查询结果进行缓存,再次访问时则直接从缓存中获取。此种方式是以数据最终一致性的思想提高响应效率,在缓存中的数据最好设置TTL值来让数据能够隔断时间重新刷新。
适用场景和优缺点
代理模式的类型很多,因而面向的场景也更为多变,它们分别适合用于不同的场合,具体的适用场景如下:
- 当需要访问远程主机中的某个对象可以使用远程代理。
- 当需要使用一个占用系统资源较少的对象来暂时代表一个占用系统资源较多的或者加载十分缓慢的对象时可以考虑使用虚拟代理。
- 当需要对某个对象的访问给予不同的权限时可以考虑使用保护代理。
- 当需要对一个对象的访问附加一些其他操作(比如记录访问行为)时可以考虑使用智能引用代理。
- 当需要为一个被频繁访问的操作结果提供一个临时存储空间,以供多个用户共享访问时可以使用缓冲代理。
优点:
- 能够协调调用者和被调用者,降低系统耦合。
- 当修改代理类不用更改原有代码,可以直接新增一个代理类,符合开闭原则,具备较好的扩展性和灵活性。
缺点:
- 在真实主题对象前增加了一个代理对象,导致访问需要转发,这中间增加了处理时间成本。
- 实现代理模式需要增加额外的工作,像远程代理、动态代理这些十分复杂的代码架构所需要付出的额外工作代价是非常大的。