我们在学习的过程中一直会遇到各种高大上的技术名词或缩写,其中动态代理就是其中之一。之前只是了解过动态代理是代理模式的一种实现,其中JDK的动态代理是基于接口的,CGlib(Code Generation Library)的动态代理是基于类的,通过动态代理可以在原本实现的基础上做到一些增强功能或额外操作,等等,可以说得头头是道,但还是不能做到知根知底,一清二楚。比如基于接口是什么意思?基于类又是什么意思?为什么?怎么个动态法?JDK的动态代理是怎么设计出来的?今天就通过博文揭开动态代理的今生前世,看看它是怎么逆袭成高大上的。
代理模式
概述
以下概念引自GOF所著《Design Patterns》的中译本及英文版。
代理模式
为其他对象提供一种代理以控制对这个对象的访问。
Proxy Pattern
Provide a surrogate or placeholder for another object to control access to it.
通俗点讲,在代理模式中,至少有两个角色:代理类proxy和被代理类real,外界对real的访问都是通过proxy传达完成的。可以说real对外界是不可见的或者透明的,因为他们对外公开的方法是一模一样的(通过实现相同的接口或继承相同的类保证)。这时proxy相当于一个中介,在外界调用真正到达real时,proxy可以做一些额外操作,比如安全验证、记录访问日志等。下面通过具体的UML图来解释。
模式的组成结构
这里以基于接口为例来讲,下面是代理模式的UML图。
代理模式包括三种角色:
- 抽象主题(Subject):抽象主题是一个接口,该接口是对象和它的代理所共用的接口,即是RealSubject和Proxy所实现的接口。
- 实际主题(RealSubject):实际主题是实现抽象主题接口的类。实际主题的实例是代理角色实例所要代理的对象。
- 代理(Proxy):代理是实现抽象主题接口的类(代理和实际主题实现了相同的接口)。代理含有主题接口声明的变量,该变量用来存放RealSubject的实例引用,这样一来,代理的实例就可以控制对它所包含的RealSubject的实例访问,即可以控制对它所代理对象的访问。
一个例子
登录注册是最常见的基本功能,如果我们还想记录登录的时间,就可以让代理类来完成。
静态代理
上边写得例子属于动态代理的前身:静态代理,静态是什么意思呢?对于上边的代码,它是写死的,被代理的方法、被代理的原始类和被代理的接口都是写死固定不变的,即login方法、UserServiceImpl类和IUserService接口是确定的。而动态代理的动态,是指这三者是不确定的,未知的,可变的,不是写死的。是不是还不太懂?那我们来想一下静态代理都有哪些缺点,为什么需要动态代理。
第一个缺点,对于接口IUserService,如果还有其他的方法,比如注册、修改个人资料等,那就要在代理类中对每一个方法进行一对一的代理,而且每一个代理方法都非常类似,结构如下:
可以想象当有很多需要被代理的方法时,会存在大量重复冗余的代码。
第二个缺点,上边第一个缺点只是针对被代理的方法需要变化时带来的不便,那如果被代理的原始类和被代理的接口也需要变化呢?比如现在不只是用户相关的接口(IUserService)需要记录访问时间,其他任何接口都需要记录,即如果当前有N个接口,每个接口中有M个方法,那按照静态代理一对一进行编码,就至少需要写N*M个代理方法,而且结构非常类似,光是想想就爆炸。
那应该怎么改进呢?
动态代理
现在我们针对上边的两个缺点,来一步步分析静态代理是怎么进化成动态代理的。如果不想写那么多重复的,想合并到一起,就需要把写死的变化的部分转化为动态可变的。
对于上边代理方法的结构,变化的部分只有一行,即调用被代理类的方法,其中被代理类target可以不同,对应的方法methodName也可以不同。静态代理中target和methodName是写死的,比如上边的UserServiceImpl和login,那能不能把target和methodName写成变量,比如String类型的,然后在运行时根据传进来的类型名和方法名实现动态调用执行呢?可以吗?有这种技术吗?机智如你有没有想到反射呢?这时上边的代理方法的结构就可以写成如下形式了(示意代码,勿抠细节)。
这样写得话就可以做到“一次编写,到处复用”了,每个代理类中所有的代理方法都可以像上边那样,调用并返回methodProxy。不同的只是:在不同的代理类中,变量targetFullName不同;在不同的方法中,变量methodName不同。这时,无论被代理的原始类和被代理的接口是谁,其对应的代理类,结构都非常类似,如下所示。
前面我们已经针对被代理的方法中把变化的部分转化为动态可变的部分,通过反射实现了被代理的方法的改善,使得方法内重复的部分“一次编写,到处复用”。那么对于上边这个代理类的结构,你有没有觉得它也有很多重复冗余的地方呢?想象现在有很多个上边这种结构的代理类,它们不同的的部分有哪些呢:1.代理类的类名ProxyClassName;2.被代理类实现的接口名SubjectInterfaceName;3.内部各个方法签名methodName0(args),methodName1(args)…;除此之外,所有的代码完全一样,那有没有什么办法能够像参照模版一样,根据给定不同的类名ProxyClassName,接口名SubjectInterfaceName自动生成如上结构的代理类代码呢?是的,你没听错,我们需要的是用代码生成代码的技术,像工厂一样批量生产代理类。可行吗?有这种技术吗?机智如你有没有想到字节码生成技术呢?完全可以按照上边的模版拼凑出我们需要的代理类,其中只需要提供需要实现的接口SubjectInterfaceName,类名ProxyClassName没有要求,我们可以自动生成,内部的所有方法名可以通过对接口进行反射获取。
这时我们改善完成的代理模式就进化成了动态代理,只需要我们自己写methodProxy方法中的额外操作,然后指定一个接口,就得到了我们需要的代理类,简单方便,主要有两个组成部分:
- 工厂:利用字节码生成技术,生成指定的代理类;
- 通用的代理方法;
本来想继续用伪码的形式写出一个结构来,但不太好表达,我们还是直接看JDK的动态代理吧,有了上边的理解,你应该就能领会到JDK设计的巧妙了。
JDK的动态代理
在JDK的动态代理实现中,对应上边工厂组件的是类Proxy,对应通用代理方法methodProxy的是接口InvocationHandler中的invoke方法。为什么是接口呢?因为这样我们可以通过实现该接口自定义需要的增强功能或额外操作,别忘了这才是目的。其中类Proxy中有一个InvocationHandler类型的成员变量h,然后类Proxy生成的代理类(extends Proxy implements SubjectInterfaceName)中,所有的方法调用,都会通过”super.h.invoke(Object, Method, object[])”这行代码委托到我们自定义的invoke方法中。下面我们使用JDK的动态代理实现最开始的登录功能,看看改进的效果。
可以看到,使用动态代理时,我们根本不用自己写代理类,只用定义我们真正需要的额外操作,然后指定需要被代理的目标类,其他的一切工作都省掉了,相比静态代理可要高明高效的多。看到这里,你可能感觉还不太舒服,因为我们没有自己写代理类,不知道它到底是什么样子,需要看一眼才放心,对吧?我们可以在上边main()方法中加入下面这句:
之后在项目目录下新建目录com\sun\proxy,然后再次运行程序,将在刚才目录下产生一个名为”$Proxy0.class”的代理类Class文件,这就是自动生成的代理类,可以使用反编译工具看一下它长什么样。
通过以上反编译得到的代码,可以观察到自动生成的代理类有以下几个特点:
- 该代理类继承Proxy类,实现了被代理类实现的接口;
- 该代理类中的所有方法调用,全部委托到父类Proxy的成员变量InvocationHandler中的invoke方法,从而执行额外操作和反射调用;
- 该代理类不仅实现了所有接口中定义的方法,还重写了Object类中的equals、hashCode、toString三个方法;
看到这里,不知道你有没有完全理解动态代理的设计和由来,反正我是懂了(哈哈。。不懂就多看几遍,多想想)。而在博文一开始提出的问题,现在也可以得到解答了。
- 为什么JDK的动态代理是基于接口的?—因为总要有一个父类或接口来定义代理类和被代理类对外公开的方法,而JDK的动态代理生成的代理类已经继承了Proxy类,所以只能通过接口来定义了。而CGlib生成的代理类,是直接继承被代理类的,所以是基于类的。
- 什么是基于接口,基于类?—从上一问的回答也可以看出,所谓的基于接口或类,即代理类和被代理类对外公开的方法是由类或接口定义的。
- 怎么个动态法?怎么设计的?—不懂得就再多看几遍,敲敲想想吧。
动态代理的优点
最后,我还想引用《深入理解JAVA虚拟机》中的原话来总结动态代理的优点,作者总结得实在是太好了,一起来感受一下吧。
动态代理中所谓的“动态”,是针对使用Java代码实际编写了代理类的“静态”代理而言的,它的优势不在于省去了编写代理类那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。
你之前是不是觉得不就省了点代码吗??哈哈,我也是。