10分钟看懂动态代理设计模式

动态代理是java语言中非常经典的一种设计模式,也是所有设计模式中最难理解的一种。本文将通过一个简单的例子模拟jdk动态代理实现,让你彻底明白动态代理设计模式的本质,文章中可能会涉及到一些你没有学习过的知识点或概念。如果恰好遇到了这些知识盲点,请先去学习这部分知识,再来阅读这篇文章。
什么是代理
从字面意思来看,代理比较好理解,无非就是代为处理的意思。举个例子,你在上大学的时候,总是喜欢逃课。因此,你拜托你的同学帮你答到,而自己却窝在宿舍玩游戏... 你的这个同学恰好就充当了代理的作用,代替你去上课。
是的,你没有看错,代理就是这么简单!
理解了代理的意思,你脑海中恐怕还有两个巨大的疑问:
怎么实现代理模式
代理模式有什么实际用途
要理解这两个问题,看一个简单的例子:
public interface flyable { void fly(); } public class bird implements flyable { @override public void fly() { system.out.println(bird is flying...); try { thread.sleep(new random().nextint(1000)); } catch (interruptedexception e) { e.printstacktrace(); } } }
很简单的一个例子,用一个随机睡眠时间模拟小鸟在空中的飞行时间。接下来问题来了,如果我要知道小鸟在天空中飞行了多久,怎么办?
有人说,很简单,在bird->fly()方法的开头记录起始时间,在方法结束记录完成时间,两个时间相减就得到了飞行时间。
@override public void fly() { long start = system.currenttimemillis(); system.out.println(bird is flying...); try { thread.sleep(new random().nextint(1000)); } catch (interruptedexception e) { e.printstacktrace(); } long end = system.currenttimemillis(); system.out.println(fly time = + (end - start)); }
的确,这个方法没有任何问题,接下来加大问题的难度。如果bird这个类来自于某个sdk(或者说jar包)提供,你无法改动源码,怎么办?
一定会有人说,我可以在调用的地方这样写:
public static void main(string[] args) { bird bird = new bird(); long start = system.currenttimemillis(); bird.fly(); long end = system.currenttimemillis(); system.out.println(fly time = + (end - start)); }
这个方案看起来似乎没有问题,但其实你忽略了准备这些方法所需要的时间,执行一个方法,需要开辟栈内存、压栈、出栈等操作,这部分时间也是不可以忽略的。因此,这个解决方案不可行。那么,还有什么方法可以做到呢?
a)使用继承
继承是最直观的解决方案,相信你已经想到了,至少我最开始想到的解决方案就是继承。 为此,我们重新创建一个类bird2,在bird2中我们只做一件事情,就是调用父类的fly方法,在前后记录时间,并打印时间差:
public class bird2 extends bird { @override public void fly() { long start = system.currenttimemillis(); super.fly(); long end = system.currenttimemillis(); system.out.println(fly time = + (end - start)); } }
这是一种解决方案,还有一种解决方案叫做:聚合,其实也是比较容易想到的。 我们再次创建新类bird3,在bird3的构造方法中传入bird实例。同时,让bird3也实现flyable接口,并在fly方法中调用传入的bird实例的fly方法:
public class bird3 implements flyable { private bird bird; public bird3(bird bird) { this.bird = bird; } @override public void fly() { long start = system.currenttimemillis(); bird.fly(); long end = system.currenttimemillis(); system.out.println(fly time = + (end - start)); } }
为了记录bird->fly()方法的执行时间,我们在前后添加了记录时间的代码。同样地,通过这种方法我们也可以获得小鸟的飞行时间。那么,这两种方法孰优孰劣呢?咋一看,不好评判!
继续深入思考,用问题推导来解答这个问题:
问题一:如果我还需要在fly方法前后打印日志,记录飞行开始和飞行结束,怎么办? 有人说,很简单!继承bird2并在在前后添加打印语句即可。那么,问题来了,请看问题二。
问题二:如果我需要调换执行顺序,先打印日志,再获取飞行时间,怎么办? 有人说,再新建一个类bird4继承bird,打印日志。再新建一个类bird5继承bird4,获取方法执行时间。
问题显而易见:使用继承将导致类无限制扩展,同时灵活性也无法获得保障。那么,使用 聚合 是否可以避免这个问题呢? 答案是:可以!但我们的类需要稍微改造一下。修改bird3类,将聚合对象bird类型修改为flyable
public class bird3 implements flyable { private flyable flyable; public bird3(flyable flyable) { this.flyable = flyable; } @override public void fly() { long start = system.currenttimemillis(); flyable.fly(); long end = system.currenttimemillis(); system.out.println(fly time = + (end - start)); } }
为了让你看的更清楚,我将bird3更名为birdtimeproxy,即用于获取方法执行时间的代理的意思。同时我们新建birdlogproxy代理类用于打印日志:
public class birdlogproxy implements flyable { private flyable flyable; public birdlogproxy(flyable flyable) { this.flyable = flyable; } @override public void fly() { system.out.println(bird fly start...); flyable.fly(); system.out.println(bird fly end...); } }
接下来神奇的事情发生了,如果我们需要先记录日志,再获取飞行时间,可以在调用的地方这么做:
public static void main(string[] args) { bird bird = new bird(); birdlogproxy p1 = new birdlogproxy(bird); birdtimeproxy p2 = new birdtimeproxy(p1); p2.fly(); }
反过来,可以这么做:
public static void main(string[] args) { bird bird = new bird(); birdtimeproxy p2 = new birdtimeproxy(bird); birdlogproxy p1 = new birdlogproxy(p2); p1.fly(); }
看到这里,有同学可能会有疑问了。虽然现象看起来,聚合可以灵活调换执行顺序。可是,为什么 聚合 可以做到,而继承不行呢。我们用一张图来解释一下:
静态代理
接下来,观察上面的类birdtimeproxy,在它的fly方法中我们直接调用了flyable->fly()方法。换而言之,birdtimeproxy其实代理了传入的flyable对象,这就是典型的静态代理实现。
从表面上看,静态代理已经完美解决了我们的问题。可是,试想一下,如果我们需要计算sdk中100个方法的运行时间,同样的代码至少需要重复100次,并且创建至少100个代理类。往小了说,如果bird类有多个方法,我们需要知道其他方法的运行时间,同样的代码也至少需要重复多次。因此,静态代理至少有以下两个局限性问题:
如果同时代理多个类,依然会导致类无限制扩展
如果类中有多个方法,同样的逻辑需要反复实现
那么,我们是否可以使用同一个代理类来代理任意对象呢?我们以获取方法运行时间为例,是否可以使用同一个类(例如:timeproxy)来计算任意对象的任一方法的执行时间呢?甚至再大胆一点,代理的逻辑也可以自己指定。比如,获取方法的执行时间,打印日志,这类逻辑都可以自己指定。这就是本文重点探讨的问题,也是最难理解的部分:动态代理。
动态代理
继续回到上面这个问题:是否可以使用同一个类(例如:timeproxy)来计算任意对象的任一方法的执行时间呢。
这个部分需要一定的抽象思维,我想,你脑海中的第一个解决方案应该是使用反射。反射是用于获取已创建实例的方法或者属性,并对其进行调用或者赋值。很明显,在这里,反射解决不了问题。但是,再大胆一点,如果我们可以动态生成timeproxy这个类,并且动态编译。然后,再通过反射创建对象并加载到内存中,不就实现了对任意对象进行代理了吗?为了防止你依然一头雾水,我们用一张图来描述接下来要做什么:
动态生成java源文件并且排版是一个非常繁琐的工作,为了简化操作,我们使用 javapoet 这个第三方库帮我们生成timeproxy的源码。希望 javapoet 不要成为你的负担,不理解 javapoet 没有关系,你只要把它当成一个java源码生成工具使用即可。
ps:你记住,任何工具库的使用都不会太难,它是为了简化某些操作而出现的,目标是简化而不是繁琐。因此,只要你适应它的规则就轻车熟路了。
第一步:生成timeproxy源码
public class proxy { public static object newproxyinstance() throws ioexception { typespec.builder typespecbuilder = typespec.classbuilder(timeproxy) .addsuperinterface(flyable.class); fieldspec fieldspec = fieldspec.builder(flyable.class, flyable, modifier.private).build(); typespecbuilder.addfield(fieldspec); methodspec constructormethodspec = methodspec.constructorbuilder() .addmodifiers(modifier.public) .addparameter(flyable.class, flyable) .addstatement(this.flyable = flyable) .build(); typespecbuilder.addmethod(constructormethodspec); method[] methods = flyable.class.getdeclaredmethods(); for (method method : methods) { methodspec methodspec = methodspec.methodbuilder(method.getname()) .addmodifiers(modifier.public) .addannotation(override.class) .returns(method.getreturntype()) .addstatement(long start = $t.currenttimemillis(), system.class) .addcode(\) .addstatement(this.flyable. + method.getname() + ()) .addcode(\) .addstatement(long end = $t.currenttimemillis(), system.class) .addstatement($t.out.println(fly time = + (end - start)), system.class) .build(); typespecbuilder.addmethod(methodspec); } javafile javafile = javafile.builder(com.youngfeng.proxy, typespecbuilder.build()).build(); // 为了看的更清楚,我将源码文件生成到桌面 javafile.writeto(new file(/users/ouyangfeng/desktop/)); return null; } }
在main方法中调用proxy.newproxyinstance(),你将看到桌面已经生成了timeproxy.java文件,生成的内容如下:
package com.youngfeng.proxy; import java.lang.override; import java.lang.system; class timeproxy implements flyable { private flyable flyable; public timeproxy(flyable flyable) { this.flyable = flyable; } @override public void fly() { long start = system.currenttimemillis(); this.flyable.fly(); long end = system.currenttimemillis(); system.out.println(fly time = + (end - start)); } }
第二步:编译timeproxy源码
编译timeproxy源码我们直接使用jdk提供的编译工具即可,为了使你看起来更清晰,我使用一个新的辅助类来完成编译操作:
public class javacompiler { public static void compile(file javafile) throws ioexception { javax.tools.javacompiler javacompiler = toolprovider.getsystemjavacompiler(); standardjavafilemanager filemanager = javacompiler.getstandardfilemanager(null, null, null); iterable iterable = filemanager.getjavafileobjects(javafile); javax.tools.javacompiler.compilationtask task = javacompiler.gettask(null, filemanager, null, null, null, iterable); task.call(); filemanager.close(); } }
在proxy->newproxyinstance()方法中调用该方法,编译顺利完成:
// 为了看的更清楚,我将源码文件生成到桌面 string sourcepath = /users/ouyangfeng/desktop/; javafile.writeto(new file(sourcepath)); // 编译 javacompilerpile(new file(sourcepath + /com/youngfeng/proxy/timeproxy.java));
第三步:加载到内存中并创建对象
url[] urls = new url[] {new url(file:/ + sourcepath)}; urlclassloader classloader = new urlclassloader(urls); class clazz = classloader.loadclass(com.youngfeng.proxy.timeproxy); constructor constructor = clazz.getconstructor(flyable.class); flyable flyable = (flyable) constructor.newinstance(new bird()); flyable.fly();
通过以上三个步骤,我们至少解决了下面两个问题:
不再需要手动创建timeproxy
可以代理任意实现了flyable接口的类对象,并获取接口方法的执行时间
可是,说好的任意对象呢?
第四步:增加invocationhandler接口
查看proxy->newproxyinstance()的源码,代理类继承的接口我们是写死的,为了增加灵活性,我们将接口类型作为参数传入:
接口的灵活性问题解决了,timeproxy的局限性依然存在,它只能用于获取方法的执行时间,而如果要在方法执行前后打印日志则需要重新创建一个代理类,显然这是不妥的!
为了增加控制的灵活性,我们考虑针将代理的处理逻辑也抽离出来(这里的处理就是打印方法的执行时间)。新增invocationhandler接口,用于处理自定义逻辑:
public interface invocationhandler { void invoke(object proxy, method method, object[] args); }
想象一下,如果客户程序员需要对代理类进行自定义的处理,只要实现该接口,并在invoke方法中进行相应的处理即可。这里我们在接口中设置了三个参数(其实也是为了和jdk源码保持一致):
proxy =>这个参数指定动态生成的代理类,这里是timeproxy
method =>这个参数表示传入接口中的所有method对象
args =>这个参数对应当前method方法中的参数
引入了invocationhandler接口之后,我们的调用顺序应该变成了这样:
myinvocationhandler handler = new myinvocationhandler(); flyable proxy = proxy.newproxyinstance(flyable.class, handler); proxy.fly(); 方法执行流:proxy.fly() =>handler.invoke()
为此,我们需要在proxy.newproxyinstance()方法中做如下改动:
在newproxyinstance方法中传入invocationhandler
在生成的代理类中增加成员变量handler
在生成的代理类方法中,调用invoke方法
public static object newproxyinstance(class inf, invocationhandler handler) throws exception { typespec.builder typespecbuilder = typespec.classbuilder(timeproxy) .addmodifiers(modifier.public) .addsuperinterface(inf); fieldspec fieldspec = fieldspec.builder(invocationhandler.class, handler, modifier.private).build(); typespecbuilder.addf...