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

动态代理是Java语言中很是经典的一种设计模式,也是全部设计模式中最难理解的一种。本文将经过一个简单的例子模拟JDK动态代理实现,让你完全明白动态代理设计模式的本质,文章中可能会涉及到一些你没有学习过的知识点或概念。若是刚好遇到了这些知识盲点,请先去学习这部分知识,再来阅读这篇文章。javascript

什么是代理

从字面意思来看,代理比较好理解,无非就是代为处理的意思。举个例子,你在上大学的时候,老是喜欢逃课。所以,你拜托你的同窗帮你答到,而本身却窝在宿舍玩游戏... 你的这个同窗刚好就充当了代理的做用,代替你去上课。php

是的,你没有看错,代理就是这么简单!java

理解了代理的意思,你脑海中恐怕还有两个巨大的疑问:git

  • 怎么实现代理模式
  • 代理模式有什么实际用途

要理解这两个问题,看一个简单的例子:程序员

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(); } } } 

很简单的一个例子,用一个随机睡眠时间模拟小鸟在空中的飞行时间。接下来问题来了,若是我要知道小鸟在天空中飞行了多久,怎么办?github

有人说,很简单,在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)); } 

这个方案看起来彷佛没有问题,但其实你忽略了准备这些方法所须要的时间,执行一个方法,须要开辟栈内存、压栈、出栈等操做,这部分时间也是不能够忽略的。所以,这个解决方案不可行。那么,还有什么方法能够作到呢?dom

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("\n") .addStatement("this.flyable." + method.getName() + "()") .addCode("\n") .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)); // 编译 JavaCompiler.compile(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.addField(fieldSpec); MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(InvocationHandler.class, "handler") .addStatement("this.handler = handler") .build(); typeSpecBuilder.addMethod(constructorMethodSpec); Method[] methods = inf.getDeclaredMethods(); for (Method method : methods) { MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(method.getReturnType()) .addCode("try {\n") .addStatement("\t$T method = " + inf.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class) // 为了简单起见,这里参数直接写死为空 .addStatement("\tthis.handler.invoke(this, method, null)") .addCode("} catch(Exception e) {\n") .addCode("\te.printStackTrace();\n") .addCode("}\n") .build(); typeSpecBuilder.addMethod(methodSpec); } JavaFile javaFile = JavaFile.builder("com.youngfeng.proxy", typeSpecBuilder.build()).build(); // 为了看的更清楚,我将源码文件生成到桌面 String sourcePath = "/Users/ouyangfeng/Desktop/"; javaFile.writeTo(new File(sourcePath)); // 编译 JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java")); // 使用反射load到内存 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(InvocationHandler.class); Object obj = constructor.newInstance(handler); return obj; } 

上面的代码你可能看起来比较吃力,咱们直接调用该方法,查看最后生成的源码。在main方法中测试newProxyInstance查看生成的TimeProxy源码:

测试代码

Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird())); 

生成的TimeProxy.java源码

package com.youngfeng.proxy; import java.lang.Override; import java.lang.reflect.Method; public class TimeProxy implements Flyable { private InvocationHandler handler; public TimeProxy(InvocationHandler handler) { this.handler = handler; } @Override public void fly() { try { Method method = com.youngfeng.proxy.Flyable.class.getMethod("fly"); this.handler.invoke(this, method, null); } catch(Exception e) { e.printStackTrace(); } } } 

MyInvocationHandler.java

public class MyInvocationHandler implements InvocationHandler { private Bird bird; public MyInvocationHandler(Bird bird) { this.bird = bird; } @Override public void invoke(Object proxy, Method method, Object[] args) { long start = System.currentTimeMillis(); try { method.invoke(bird, new Object[] {}); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); } } 

至此,整个方法栈的调用栈变成了这样:


 
 

看到这里,估计不少同窗已经晕了,在静态代理部分,咱们在代理类中传入了被代理对象。但是,使用newProxyInstance生成动态代理对象的时候,咱们竟然再也不须要传入被代理对象了。咱们传入了的实际对象是InvocationHandler实现类的实例,这看起来有点像生成了InvocationHandler的代理对象,在动态生成的代理类的任意方法中都会间接调用InvocationHandler->invoke(proxy, method, args)方法。

其实的确是这样。TimeProxy真正代理的对象就是InvocationHandler,不过这里设计的巧妙之处在于,InvocationHandler是一个接口,真正的实现由用户指定。另外,在每个方法执行的时候,invoke方法都会被调用 ,这个时候若是你须要对某个方法进行自定义逻辑处理,能够根据method的特征信息进行判断分别处理。

如何使用

上面这段解释是告诉你在执行Proxy->newProxyInstance方法的时候真正发生的事情,而在实际使用过程当中你彻底能够忘掉上面的解释。按照设计者的初衷,咱们作以下简单概括:

  • Proxy->newProxyInstance(infs, handler) 用于生成代理对象
  • InvocationHandler:这个接口主要用于自定义代理逻辑处理
  • 为了完成对被代理对象的方法拦截,咱们须要在InvocationHandler对象中传入被代理对象实例。

查看上面的代码,你能够看到我将Bird实例已经传入到了MyInvocationHandler中,缘由就是第三点。

这样设计有什么好处呢?有人说,咱们大费周章,饶了一大圈,最终变成了这个样子,到底图什么呢?

想象一下,到此为止,若是咱们还须要对其它任意对象进行代理,是否还须要改动newProxyInstance方法的源码,答案是:彻底不须要!

只要你在newProxyInstance方法中指定代理须要实现的接口,指定用于自定义处理的InvocationHandler对象,整个代理的逻辑处理都在你自定义的InvocationHandler实现类中进行处理。至此,而咱们终于能够从不断地写代理类用于实现自定义逻辑的重复工做中解放出来了,今后须要作什么,交给InvocationHandler。

事实上,咱们以前给本身定下的目标“使用同一个类来计算任意对象的任一方法的执行时间”已经实现了。严格来讲,是咱们超额完成了任务,TimeProxy不只能够计算方法执行的时间,也能够打印方法执行日志,这彻底取决于你的InvocationHandler接口实现。所以,这里取名为TimeProxy其实已经不合适了。咱们能够修改成和JDK命名一致,即$Proxy0,感兴趣的同窗请自行实践,本篇文章的代码将放到个人Github仓库,文章结尾会给出代码地址。

JDK实现揭秘

经过上面的这些步骤,咱们完成了一个简易的仿JDK实现的动态代理逻辑。接下来,咱们一块儿来看一看JDK实现的动态代理和咱们到底有什么不一样。

Proxy.java

 
 

 

InvocationHandler

 
 

 

能够看到,官方版本Proxy类提供的方法多一些,而咱们主要使用的接口newProxyInstance参数也和咱们设计的不太同样。这里给你们简单解释一下,每一个参数的意义:

  • Classloader:类加载器,你可使用自定义的类加载器,咱们的实现版本为了简化,直接在代码中写死了Classloader。
  • Class<?>[]:第二个参数也和咱们的实现版本不一致,这个其实很容易理解,咱们应该容许咱们本身实现的代理类同时实现多个接口。前面设计只传入一个接口,只是为了简化实现,让你专一核心逻辑实现而已。

最后一个参数就不用说了,和咱们实现的版本彻底是同样的。

仔细观察官方版本的InvocationHandler,它和咱们本身的实现的版本也有一个细微的差异:官方版本invoke方法有返回值,而咱们的版本中是没有返回值的。那么,返回值到底有什么做用呢?直接来看官方文档:


 
 

核心思想:这里的返回值类型必须和传入接口的返回值类型一致,或者与其封装对象的类型一致。

遗憾的是,这里并无说明返回值的用途,其实这里稍微发挥一下想象力就知道了。在咱们的版本实现中,Flyable接口的全部方法都是没有返回值的,问题是,若是有返回值呢?是的,你没有猜错,这里的invoke方法对应的就是传入接口中方法的返回值。

答疑解惑

invoke方法的第一个参数proxy到底有什么做用?

这个问题其实也好理解,若是你的接口中有方法须要返回自身,若是在invoke中没有传入这个参数,将致使实例没法正常返回。在这种场景中,proxy的用途就表现出来了。简单来讲,这其实就是最近很是火的链式编程的一种应用实现。

动态代理到底有什么用?

学习任何一门技术,必定要问一问本身,这到底有什么用。其实,在这篇文章的讲解过程当中,咱们已经说出了它的主要用途。你发现没,使用动态代理咱们竟然能够在不改变源码的状况下,直接在方法中插入自定义逻辑。这有点不太符合咱们的一条线走到底的编程逻辑,这种编程模型有一个专业名称叫 AOP。所谓的AOP,就像刀同样,抓住时机,趁机插入。

 
 

 

基于这样一种动态特性,咱们能够用它作不少事情,例如:

  • 事务提交或回退(Web开发中很常见)
  • 权限管理
  • 自定义缓存逻辑处理
  • SDK Bug修复
    ...

若是你阅读过 Android_Slide_To_Close 的源码会发现,它也在某个地方使用了动态代理设计模式。

总结

到此为止,关于动态代理的全部讲解已经结束了,原谅我使用了一个诱导性的标题“骗”你进来阅读这篇文章。若是你不是一个久经沙场的“老司机”,10分钟彻底看懂动态代理设计模式仍是有必定难度的。但即便没有看懂也不要紧,若是你在第一次阅读完这篇文章后依然一头雾水,就不妨再仔细阅读一次。在阅读的过程当中,必定要跟着文章思路去敲代码。反反复复,必定会看懂的。我在刚刚学习动态代理设计模式的时候就反复看了不下5遍,而且亲自敲代码实践了屡次。

为了让你少走弯路,我认为看懂这篇文章,你至少须要学习如下知识点:

  • 至少已经理解了面向对象语言的多态特性
  • 了解简单的反射用法
  • 会简单使用 JavaPoet 生成Java源码

若是你在阅读文章的过程当中,有任何不理解的问题或者建议,欢迎在文章下方留言告诉我!

做者:欧阳锋连接:https://www.jianshu.com/p/fc285d669bc5

相关文章
相关标签/搜索