代理模式,也称委托模式,是结构型设计模式之一,何为代理呢? 在日常生活中就比如叫朋友替你拿个快递,叫朋友替你做一下作业,叫朋友替你买点东西等等,这个朋友就是你的代理,你把事情委托你的朋友做了,同样在代码的世界中也存在代理,而且在你以后阅读到更多的设计模式时,你会发现很多的设计模式中也有代理模式的影子,代理模式是一个非常重要的设计模式,代理模式分为静态代理和动态代理,本文将会通过一个简单的例子讲解静态代理,然后引出动态代理,并且深入的探讨一下动态代理的实现原理。
代理模式的定义
为其他对象提供一种代理以控制这个对象的访问。
使用场景
静态代理和动态代理都适用于以下场景:
1、当不想访问某个对象或访问某个对象存在困难时,就可以为这个对象创建一个代理,通过代理来间接的访问这个对象;
2、如果原始对象有不同的访问权限,可以使用代理控制对原始对象的访问,保护原始对象;
3、在访问原始对象时执行一些自己的附加操作;
4、为某个对象在不同的内存地址空间提供局部代理,使得系统可以将服务端的实现隐藏,客户端不必考虑服务端的存在,例如Android中的Binder。
下面先简单的讲解一下静态代理。
静态代理
1、类图
角色介绍:
- Subject --- 抽象主题类,定义了代理对象和真实对象的共同接口方法,既可以是接口也可以是抽象类。
- RealSubject --- 真实主题类,该类可以被称为被委托类或被代理类,该类定义了代理对象所表示的真实对象,实现了Subject接口,而Client端通过代理类间接的调用的真实主题类中的方法,由其执行真正的业务逻辑。
- ProxySubject --- 代理类,该类也被称为委托类或代理类,该类中持有一个真实主题类的引用,同样实现了Subject接口,在其实现的接口方法中调用真实主题类中相应的接口方法,以此起到代理的作用。
- Client --- 客户端,使用代理。
这个类图就是静态代理的结构,在下面小明通过中介租房的例子中会有体现这几个角色的作用,而动态代理的类图结构与静态代理的稍有不同,将会在动态代理那里讲到,但动态代理和静态代理的整体思想是相同的,我们还需要注意一下下面会提到的一些近义词:
Subject = 公共接口;
ProxySubject = 代理对象 = 代理类 = 委托类 = 代理人;
RealSubject = 真实对象 = 被代理类 = 被委托类 = 被代理人;
在根据不同的上下文时我会用不同的词表示Subject 、ProxySubject 和 RealSubject,还有委托和代理这个两个动词要根据上下文含义理解。
2、使用静态代理
使用静态代理的基本步骤:
1、定义代理对象和真实对象的公共接口;
2、真实对象实现公共接口中的方法;
3、代理对象实现公共接口中的方法,并把方法的逻辑转发给真实对象。
我们通过小明买房的这个例子来讲解静态代理,小明想要在大城市租房,但是他平时很忙没有时间去看房,于是他就找到一个房产中介,把自己的租房意愿告诉房产中介,让房产中介来替自己解决租房问题,很明显房产中介就是代理人,小明就是被代理的人。
我们用静态代理来实现这个过程,首先定义一个租房步骤的公共接口:
4个步骤完成租房,很简单,然后我们定义具体的想要租房的人即小明:
该类实现了IRoom接口,实现了其中的具体逻辑,但是小明并不会自己去打租房,他委托房产中介去做,所以这里定义一个房产中介:
在该类中会持有一个被代理人的引用,在这里指小明,可以看到房产中介所执行的方法的实质就是简单的调用被代理人中的方法,下面来看看Client中具体的执行关系:
输出结果:
上面就是傻瓜式的过程,一看就懂,房产中介代理了小明的找房、看房、租房等过程,可以看到静态代理模式还是很简单,就是一种委托机制,真实对象将方法委托给代理对象,那么房产中介继续代理其他人可以吗? 可以的,比如XiaoHong也想租房,我们再定义一个XiaoHong实现IRoom接口,并在Client中给房产中介RoomAgency代理就行。
3、缺点
但是如果小明是想要买房而不是租房,这时房产中介还能满足小明的需求吗?很显然不能了,因为这个房产中介它只有替人租房的能力,没有替人买房的能力,这时就需要更换租房接口为买房接口,再定义一个专门买房的的房产中介,你会发现我每次更换接口,都需要更换代理类,这就是静态模式的缺点,只能为给定接口下的实现类做代理,如果接口不同就需要定义不同的代理类,随着系统的复杂度增加,就会很难维护这么多代理类和被代理类之间的关系,这时动态代理就应运而生,当需要频繁的更换接口,更换代理类时,采用动态代理是一个更好的选择,动态代理可以通过一个代理类来代理N多个被代理类,它在更换接口时,不需要重新定义代理类,因为动态代理不需要根据接口提前定义代理类,它把代理类的创建推迟到代码运行时来完成。
4、与动态代理的区别
我们先来复习一下class文件的加载,我们编写的.Java文件经过javac编译之后,会产生.class文件,这种.class文件是二进制文件,里面的内容是只有JVM能够识别,在代码运行之前,JVM会读取.class文件,解析.class文件内的信息,取出二进制数据,加载进内存中,从而生成对应的Class对象。
而静态代理和动态代理最主要的区别就是:静态代理在我们的代码运行之前,代理类的.class文件就已经存在,例如上述的RoomAgency.java,在经过javac编译之后,就会变成RoomAgency.class;而动态代理则与静态代理相反,在代码运行之前不存在代理类的.class文件,在代码运行时才动态的生成代理类。
下面来讲解动态代理,并通过动态代理重新实现一遍小明买房的例子。
动态代理
1、类图
这个就是动态代理的大概类图结构,其中Subject 、ProxySubject、 RealSubject和Client角色的作用和静态代理的一样,这里就不在累述,与静态代理相比,多了一个InvocationHandler角色和一个Proxy角色,InvocationHandler是java提供的一个接口,我们需要定义一个类实现InvocationHandler接口,这里就叫DynamicProxy角色;Proxy是java提供用于动态生成ProxySubject的一个类,它需要ProxySubject继承。
我们看到DynamicProxy在ProxySubject和RealSubject之前起到了中间人的角色,ProxySubject会把事情委托给DynamicProxy来做,而DynamicProxy最终把事情委托给RealSubject来做,可以这样说:ProxySubject代理了DynamicProxy,而DynamicProxy代理了RealSubject,其中最重要的一点是ProxySubject是在代码运行时才动态生成的,这是和静态代理的最大区别。
接下来简单介绍一下InvocationHandler接口,Proxy类。
1、InvocationHandler和Proxy的作用
为了让我们更加容易的实现动态代理,java提供了动态代理接口InvocationHandler和动态代理类Proxy供我们使用,它们都在java.lang.reflect包中,可见动态代理和反射有不可逃脱的关系。
InvocationHandler接口 定义如下:
InvocationHandler接口的作用就是在invoke方法中执行真实对象的方法,可以看到里面只有一个invoke方法,我们需要为真实对象定义一个实现了这个接口中的invoke方法的动态代理类,同时在创建这个动态代理类的实例的时候,我们还要在方法或构造中传入真实对象的引用,即InvocationHandler的实现类需要持有真实对象的引用,这样才能执行真实对象的方法。
Proxy类定义如下:
Proxy这个类的作用就是用来动态的创建一个代理对象,它内部会持有一个InvocationHandler引用,在构造中传入,它提供了许多的方法,但是我们常用的是 getProxyClass方法和newProxyInstance方法:
- getProxyClass(重点方法):这个方法的作用是在运行时根据.class的结构生成一个代理Class二进制流,并通过传入的ClassLoader去把代理Class二进制流加载成一个代理Class对象,该代理Class对象继承Proxy并实现了传入的第二个参数对应的Interface列表。
- newProxyInstance(常使用的方法): 这个方法的作用是在运行时根据代理Class对象生成代理对象实例,这个方法中会先调用了getProxyClass方法生成代理Class对象,在获取到代理Class对象后,根据第三个参数InvocationHandler引用通过反射创建代理对象实例,所以newProxyInstance最终的结果是生成一个代理对象实例,该代理对象会继承Proxy类并实现给定的接口列表,同时内部持有一个InvocationHandler引用。
以上两个方法过程现在看不懂不要紧,下面在讲解动态代理的源码分析时还会再分析一遍,我们通常会使用newProxyInstance方法来生成一个代理对象实例。
3、使用动态代理
使用动态代理的基本步骤如下:
1、定义代理对象和真实对象的公共接口;(与静态代理步骤相同)
2、真实对象实现公共接口中的方法;(与静态代理步骤相同)
3、定义一个实现了InvocationHandler接口的动态代理类;
4、通过Proxy类的newProxyInstance方法创建代理对象,调用代理对象的方法。
1和2步骤都是和静态代理步骤相同的,就不在累述了,和静态代理相比,少了的一个步骤是:代理对象实现公共接口的方法,因为前面讲过代理对象是代码运行时通过Proxy动态创建的,所以不需要提前编写代理对象的类;和静态代理相比,多了的两个步骤是:3、定义一个实现了InvocationHandler接口的动态代理类和4、通过Proxy类的newProxyInstance方法创建代理对象,调用代理对象的方法,我们接着静态代理的小明买房的例子,下面分别讲解:
步骤3:我们需要定义一个动态代理类,它用于执行真实对象的方法:
在该类中,我们声明了一个Object引用,该引用指向真实对象,真实对象在构造函数中传入,而在invoke方法中通过反射调用真实对象的具体方法,这里需要注意的是指向真实对象的引用类型最好定义为Objec类型t而不是真实对象的具体类型如XiaoMing,这样做的好处是,当你要代理另外一个人时,例如xiaoHong,我在DynamicProxy的构造函数中只需要传入xiaoHong引用而不用更改DynamicProxy的类结构,这样一个DynamicProxy就可以代理很多人。
接着步骤4:通过Proxy类newProxyInstance方法创建代理对象,调用代理对象的方法,下面是Client端逻辑
运行结果和前面的静态代理一致,就不再贴出,在介绍Proxy时讲过,Proxy的newProxyInstance方法会根据传入的类加载器动态生成代理对象实例,生成的代理对象会继承Proxy类并实现传入的接口列表,这里的类加载器是小明的ClassLoader,即真实对象的类加载器,而接口列表则是IRoom,传入的IRoom的Class对象,除了这个两个参数,还传入了动态代理类InvocationHandler实例,这样Proxy类在创建代理对象的实例时就会把这个InvocationHandler引用传给代理对象,接下来当我们调用代理对象的方法时,这个方法的处理逻辑就会委托给InvocationHandler实例的invoke方法执行,invoke方法中就会通过反射调用我们真实对象的方法。
下面我们通过源码看一下是怎样生成代理对象以及生成的代理对象是长什么样的。
4、源码分析
我们看Client的注释1:
我们先看Client的注释1,Proxy的newProxyInstance方法会根据传入的类加载器动态生成代理对象实例,我们点进Proxy的newProxyInstance方法看一下,如下:
这个方法里面的流程还是很简单的,首先注释1,调用getProxyClass0,获得一个代理Class对象,getProxyClass0等于前面讲过的getProxyClass的作用,如下
可以看到,getProxyClass里面也是简单的调用getProxyClass0方法,所以getProxyClass0方法的作用就是我前面讲的getProxyClass方法的作用:在运行时根据.class的结构生成一个代理Class二进制流,并通过传入的ClassLoader去把代理Class二进制流加载成一个代理Class对象,该代理Class对象继承Proxy并实现了传入的第二个参数对应的Interface列表。
我们来看一下这个动态生成的代理Class对象的真实面目,首先在Client的main函数的开头填入下面的一代码:
填入后,运行Client的main函数就会在你的idea工作空间下的com/sun/proxy/目录下生成一个$Proxy0.class文件,这个$Proxy0.class就是动态生成的代理Class对象,即代理对象,如下:
这个.class文件里面都是JVM才能看懂的二进制,用idea打开,它会自动替你反编译成.java文件,如下:
为了阅读方便,我省略了无关代码,可以看到Proxy类的getProxyClass0方法会替我们动态生成代理对象$Proxy0.class,这个代理对象会继承Proxy类,和实现接口列表,而这里传入的接口只有IRoom,所以$Proxy0会实现IRoom的方法,这些方法里面的逻辑都是调用父类的h的invoke方法,父类的h就是InvocationHandler引用,我们回去看newProxyInstance方法的注释2和3,你就会发现这个InvocationHandler引用是在通过反射创建$Proxy0实例时在构造中传入的。
我们在$Proxy0中还发现了很多Method对象,在$Proxy0的底部的static块中通过反射获取到我们IRoom接口所有方法的Method对象,当我们调用某个方法时,相应方法的method 和 代理对象$Proxy0实例、还有方法参数一起传进了父类的h的invoke方法中,所以我们在invoke方法中就可以根据method通过反射调用真实对象的相应方法,如下:
我们回到Client的注释1,所以当我们调用Proxy类的newProxyInstance方法,这个方法在里面创建了代理对象,并返回代理对象$Proxy0实例,所以当我们调用代理对象的方法时,我们就是在调用$Proxy0相应的方法,这个方法处理逻辑就会委托给InvocationHandler实例的invoke方法执行(代理对象的父类持有InvocationHandler引用),invoke方法中就会通过反射调用我们真实对象的方法(InvocationHandler的实现类中持有真实对象的引用),这就是整个动态代理的过程。
5、原理
通过使用和源码分析,相信大家对动态代理有一个更加深入的了解,动态代理的原理就是通过反射机制动态生成代理类,这是因为由于JVM可以通过.class文件的二进制信息加载class对象的,那么,如果我们在代码运行时,遵循.class文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据通过JVM加载成对应的class对象,有了class对象,我们就可以在运行时通过反射创建出代理对象的实例,这样就完成了在代码运行时,动态的创建一个代理对象的能力,这就是动态代理的原理。
结语
在静态代理模式中,代理类ProxySubject中的方法,都指定地调用了特定ReadSubject对应的方法;而在动态代理模式中,代理类ProxySubject中每一个方法的调用,都会交给InvocationHandler来处理,而InvocationHandler则调用了RealSubject的方法,以上就是我对静态代理和动态代理的理解,下面用一张表总结本文:
|
优点 |
缺点 |
区别 |
动态代理 |
1、代理类在程序运行时由反射自动生成,无需我们手动编写代理类代码,简化编程工作 2、一个动态代理类InvocationHandler就能代理多个被代理类,较为灵活 |
1、动态代理只能代理实现实现了接口的类,而不能代理实现抽象类的类 2、通过反射调用被代理类的方法,效率低 |
需要提前实现接口编写代理类,在代码运行之前,代理类的.class文件就已经存在 |
静态代理 |
1、代理类作为客户端和被代理类之间的中介,起到了保护被代理类的作用 2、通过接口对代理类和被代理类进行解耦,降低了系统的耦合度 |
1、只能为给定接口下的实现类做代理,如果接口不一样那么就要重新定义不同的代理类,维护复杂 2、由于在客户端和被代理类之间增加了代理对象,因此会造成请求的处理速度变慢 |
不需要提前实现接口编写代理类,在代码运行时,由JVM来动态的创建代理类 |
代理模式应用广泛,在实际开发中要根据实际情况进行选择。