关于动态代理,我之前写过一篇文章《10分钟看懂动态代理设计模式》 。在这篇文章中,我收到了一些关于动态代理的提问,也有同学在微信公众号给我私信,询问关于动态代理的问题。再次Review这篇文章之后,我发现了一些问题。确实有一些细节没有介绍清楚,有一些地方含混过关了。因此,我决定重新写一篇关于动态代理的文章,希望可以讲清楚关于动态代理实现的每一处细节。这注定又是一场艰难的旅程,你愿意加入我们吗?
这篇文章的思路还是同之前的文章一样,由浅入深,如果你已经看过上一篇文章,部分章节可以跳过。
静态代理 那么,到底什么是代理呢?
所谓的代理,其实就是中间人的意思。例如:让朋友代替你去取快递,你的朋友就充当了代理的作用。再比如,让你的朋友帮你去借款,你的朋友实际上也充当了代理的作用,最终这笔钱的受益人还是你自己。
理解了代理的意思,接下来我们一起来看一下,在面向对象编程语言中,到底应该如何体现代理呢。
这里我们就以上面提到的代取快递为例,来写一个简单的代理实现。
我们用Friend
类表示你的朋友,用Self
表示你自己,上面的例子用代码实现应该是这样:
1 2 3 4 5 6 7 8 9 10 11 public class Friend { private String name; public Friend (String name ) { this .name = name; } public void collectPack ( ) { System.out .println(this .name + "去取快递..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Self { private Friend friend ; public Self (Friend friend ) { this .friend = friend ; } public void collectPack () { friend .collectPack(); } public static void main (String[] args) { Friend friend = new Friend("张三" ); Self self = new Self(friend ); self.collectPack(); } }
在上面这段代码中,我们实现了一个简单的代理。这里的代理类是Friend
,被代理类是Self
,通过这段代码,我们知道了一个基本事实:代理对象 是真正去执行动作的对象,被代理对象 是被动执行动作的对象(并不真正执行动作)。
上面的实现看似没有问题,实际上却不够友好,由于代理对象能够替被代理对象执行动作。所以,他们应该具有同样的一些方法。换句话说,应该实现他们应该实现同样的接口,这个接口中的方法表示双方都可以执行的一些动作,或者说可能要被代理的一些动作。
因此,上面的代码可以改写成下面这样:
1 2 3 4 5 public interface Collectable { void collectPack ( ) ; }
1 2 3 4 5 6 7 8 9 10 11 public class Friend implements Collectable { private String name; public Friend (String name) { this .name = name; } public void collectPack () { System.out.println(this .name + "去取快递..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Self implements Collectable { private Friend friend ; public Self (Friend friend ) { this .friend = friend ; } public void collectPack () { friend .collectPack(); } public static void main (String[] args) { Friend friend = new Friend("张三" ); Self self = new Self(friend ); self.collectPack(); } }
恭喜你!通过上面的改造,我们已经完成了一个标准的静态代理实现。之所以称之为静态代理,是因为这里的逻辑是写死的,并不具备动态特性。与之相对的,就是今天这篇文章的主角:动态代理。
代理的作用是什么 看到这里,应该有人会问了,说了这么多,代理到底有什么用呢。这个问题并不容易回答,为了回答你的这个问题,我们先来看一个场景。
假设有一个类Driver
,类中只有一个方法drive
,我们不能改动这个类的源码,如何获取这个方法的执行时间呢?
1 2 3 4 5 6 7 8 public class Driver { public void drive ( ) throws InterruptedException { System.out .println("I'm driving..." ); Thread.sleep(1000 ); System.out .println("Drive completed..." ); } }
有的同学可能会说,这还不简单,在main方法中,方法执行前记录一个时间,方法执行后记录一个时间,两个时间相减就得到了方法最终的执行时间。
位置p1
1 2 3 4 5 6 7 8 9 public static void main(String [] args) throws InterruptedException { Driver driver = new Driver(); long start = System.currentTimeMillis(); driver.drive(); long end = System.currentTimeMillis(); System.out.println (end - start); }
这个方法到底对不对呢,先说结论:不对!这里的时间会大于方法实际执行的时间,因为这里包含了准备方法的那些时间。
除了这个方法之外,还有一个比较容易想到的方法就是继承 。通过继承Driver类,在drive方法前后打印时间,计算时间差,这种方式似乎可行!
1 2 3 4 5 6 7 8 9 10 public class Driver1 extends Driver { @Override public void drive() throws InterruptedException { long start = System .currentTimeMillis(); super .drive(); long end = System .currentTimeMillis(); System .out.println(end - start); } }
位置p2
1 2 3 4 public static void main (String[] args) throws InterruptedException { Driver1 driver1 = new Driver1(); driver1.drive(); }
这里有一个疑点,有些同学在问,到底方法的准备时间是什么,为什么会有准备方法的这些时间。这两种方式不是都需要准备方法吗?
上面我们说到,直接打印的方式包含了准备方法的那些时间,准备一个方法通常需要先压栈,调用后自动出栈,这些都需要时间,尤其在一些性能比较低的机器上会体现的特别明显。不信,大家可以执行位置p1处的代码与位置p2处的代码,你会发现,位置p2处的执行时间几乎总是比位置p1处的执行时间少3~5毫秒(在我的Macbook Pro 15.4 2017上执行是这样的结果,其它机型时间可能略有差异)。在继承中不需要这些时间的原因是:我们是方法内部执行的,这个时候方法已经准备好,就不存在这个准备时间了。
Ok,说完了上面这个问题,我们继续回到上面的代码。在上面的代码中,我们通过继承的方式获得了方法的执行时间。接下来,新需求来了,我要你在drive
方法前后各打印一条日志。
你会怎么做呢,当然毫无疑问,我们继续沿用上面的解决方案,继承Driver类创建新类Driver2。然后,在super调用前后各打印一条日志。
1 2 3 4 5 6 7 8 9 public class Driver2 extends Driver { @Override public void drive() throws InterruptedException { System .out.println("Drive start..." ); super .drive(); System .out.println("Drive complete..." ); } }
问题似乎很简单,可是,新的需求又来了。我要你先打印日志再获取方法的执行时间。其实,这也很简单。我们只要继承上面的Driver2
类,重写drive
方法获取方法执行的时间即可:
1 2 3 4 5 6 7 8 9 10 public class Driver3 extends Driver2 { @Override public void drive() throws InterruptedException { long start = System .currentTimeMillis(); super .drive(); long end = System .currentTimeMillis(); System .out.println(end - start); } }
接下来,麻烦来了。需求又变了,我要你先获取方法的执行时间再打印日志,怎么办。
有人说,这也没毛病啊。我们只要继承Driver1
创建新类Driver4
,然后在drive
方法中打印日志即可:
1 2 3 4 5 6 7 8 9 public class Driver4 extends Driver1 { @Override public void drive() throws InterruptedException { System .out.println("Drive start..." ); super .drive(); System .out.println("Drive complete..." ); } }
也许你已经发现了,这个解决方案存在着明显的问题,如果一个类有100个方法,实现上述这些逻辑大概需要创建400个类,这显然不是一个可取的方法。那么,是否有更好的解决方案呢?
在上面的解决方案中,我们通过继承的方式获取到了父类方法的执行时间。但是,如果Driver类被final修饰呢,大家知道final类是无法被继承的,继承这条路显然走不通了。
但这似乎恰好为我们打开了一扇窗,我们尝试使用文章开头静态代理的方式传入不同功能的Driver类实例,看看能否发生一些特殊的化学反应。
同样,我们以获取方法的执行时间为例,如果要通过静态代理的方式获取方法的执行时间,我们应该这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Driver5 { private Driver driver; public Driver5 (Driver driver ) { this .driver = driver; } public void drive ( ) throws InterruptedException { long start = System.currentTimeMillis(); driver.drive(); long end = System.currentTimeMillis(); System.out .println(end - start); } }
前面说到,我们应该将统一的动作抽象化。因此,这里我们新增统一的接口Drivable
,我们让所有的Driver类都实现Drivable
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Driver5 implements Drivable { private Driver driver; public Driver5 (Driver driver) { this .driver = driver; } public void drive () throws InterruptedException { long start = System.currentTimeMillis(); driver.drive(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
同样地,为了在方法的执行前后打印日志,我们创建Driver6
通过代理的方式实现日志的打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Driver6 implements Drivable { private Driver driver; public Driver6 (Driver driver) { this .driver = driver; } public void drive () throws InterruptedException { System.out.println("Drive start..." ); driver.drive(); System.out.println("Drive complete..." ); } }
为了与前面的方式区分开来,我们将Driver5
重命名为DriverTimeProxy
,Driver6
重命名为DriverLogProxy
,接下来我们尝试先打印日志再获取方法的执行时间。
咋一看,似乎毫无头绪。别急,我们先尝试将聚合对象Driver
抽象化。参数类型修改为Drivable
(由于Driver类也实现了Drivable接口),修改完成后代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class DriverLogProxy implements Drivable { private Drivable drivable; public DriverLogProxy (Drivable drivable) { this .drivable = drivable; } public void drive () throws InterruptedException { System.out.println("Drive start..." ); drivable.drive(); System.out.println("Drive complete..." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DriverTimeProxy implements Drivable { private Drivable drivable; public DriverTimeProxy (Drivable drivable) { this .drivable = drivable; } public void drive () throws InterruptedException { long start = System.currentTimeMillis(); drivable.drive(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
至此,神奇的化学反应出现了。由于DriverLogProxy
与DriveTimeProxy
都实现了Drivable
接口,我们可以将这两个对象相互聚合到对方的类中。
例如,如果我们要先打印方法的执行时间再打印日志,可以这样做:
1 2 3 4 5 6 7 8 public static void main(String [] args) throws InterruptedException { Driver driver = new Driver (); DriverTimeProxy driverTimeProxy = new DriverTimeProxy (driver); DriverLogProxy driverLogProxy = new DriverLogProxy (driverTimeProxy); driverLogProxy.drive(); }
而如果我们要先打印日志再打印方法的执行时间,可以这样做:
1 2 3 4 5 6 7 8 public static void main(String [] args) throws InterruptedException { Driver driver = new Driver (); DriverLogProxy driverLogProxy = new DriverLogProxy (driver); DriverTimeProxy driverTimeProxy = new DriverTimeProxy (driverLogProxy); driverTimeProxy.drive(); }
在前面的例子中,为了实现相同的功能,我们至少需要创建四个类,而这里似乎只需要两个类就搞定了。显然,这种通过代理处理的方式更优。
动态代理 在上面的例子中,我们通过静态代理仅使用两个类就完成了四种不同顺序调用的组合,这种在一个类中持有另一个类的实例引用的方式也被称之为聚合 。在这种场景中,我们可以说,聚合优于继承。
接下来,我们继续加大问题的难度,我们是否可以在任意对象的任意方法前后添加任意的操作呢?并且不需要增加额外的类。
这个问题的难度一下提高了不少,为了简化问题的难度,我们将问题分解一下。
首先,第一个要点是,必须在任意对象的任意方法前后执行任意的操作,这就要求我们必须拿到对象中的任意方法。要拿到对象的任意方法怎么做呢,反射恰好可以解决你的问题。
第二个要点,不能增加额外的类,换句话说,DriverLogProxy与DriverTimeProxy都不需要增加,怎么办!这个地方不太容易想到,其实我们可以帮助用户生成这样的类,使用Java代码编译并通过ClassLoader将字节码加载到内存中,再通过反射的方式进行调用。这样看起来虽然生成了额外的类,但用户是无法感知的,也就做到了不增加额外类的要求。
接下来,我们先来尝试动态生成针对时间的代理类DriverTimeProxy,这个类的完整代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DriverTimeProxy implements Drivable { private Drivable drivable; public DriverTimeProxy (Drivable drivable) { this .drivable = drivable; } public void drive () throws InterruptedException { long start = System.currentTimeMillis(); drivable.drive(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
为了生成这样的代码,我们需要分两步处理:
第一步:将上面的代码当成字符串拼接到变量str中
第二步:使用File类将文件输出到硬盘
以上是我们生成DriverTimeProxy
类源码的基本思路,为了简化第一步的处理,这里我们使用JavaPoet 来处理。
JavaPoet是什么呢,JavaPoet其实就是一个Java源码生成工具,为了让大家对JavaPoet有一个更直观的了解,我们先来看一段使用JavaPoet编写的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MethodSpec main = MethodSpec.methodBuilder("main" ) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args" ) .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!" ) .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld" ) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out);
将以上代码复制到main方法中执行,你将得到下面这样的输出结果:
1 2 3 4 5 6 7 package com.example.helloworld;public final class HelloWorld { public static void main (String[] args) { System.out.println("Hello, JavaPoet!" ); } }
怎么样,明白了吧。通过使用上面的一段API对Java代码进行描述,JavaPoet就自动帮我们生成了一段非常漂亮的Java代码,连排版都省了,这就是JavaPoet的作用。
Ok,接下来我们增加下面这段代码帮助我们生成DriverTimeProxy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 MethodSpec drive = MethodSpec.methodBuilder("drive" ) .addModifiers (Modifier.PUBLIC) .addAnnotation (Override.class) .addException (InterruptedException.class) .addStatement ("long start = $T.currentTimeMillis()" , System.class) .addStatement ("drivable.drive()" ) .addStatement ("long end = $T.currentTimeMillis()" , System.class) .addStatement ("$T.out.println(end - start)" , System.class) .build ()FieldSpec fieldSpec = FieldSpec.builder(Drivable.class, "drivable" ) .addModifiers (Modifier.PRIVATE) .build ()MethodSpec constructor = MethodSpec.constructorBuilder() .addModifiers (Modifier.PUBLIC) .addParameter (Drivable.class, "drivable" ) .addStatement ("this.$N = $N" , "drivable" , "drivable" ) .build ()TypeSpec driverTimeProxy = TypeSpec.classBuilder("DriverTimeProxy" ) .addModifiers (Modifier.PUBLIC) .addSuperinterface (Drivable.class) .addMethod (drive) .addMethod (constructor) .addField (fieldSpec) .build ()JavaFile javaFile = JavaFile.builder("com.youngfeng.designmode.proxy" , driverTimeProxy).build() javaFile.writeTo(System.out)
在main运行以上代码,你将得到下面这样的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.youngfeng.designmode.proxy;import com.youngfeng.designmode.proxy.juhe.Drivable;import java.lang.InterruptedException;import java.lang.Override;import java.lang.System;public class DriverTimeProxy implements Drivable { private Drivable drivable; public DriverTimeProxy (Drivable drivable) { this .drivable = drivable; } @Override public void drive () throws InterruptedException { long start = System.currentTimeMillis(); drivable.drive(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
这恰好就是DriverTimeProxy
类的完整代码。接下来,我们继续尝试第二步,将生成的DriverTimeProxy类动态编译并加载到内存中,同时通过反射的方式创建该对象。为了让大家看的更清晰,我们创建一个新类Proxy
并增加一个静态方法newProxyInstance
专门用来处理这个问题。
为了实现动态编译,我们需要将文件输出到硬盘中,简单起见,这里我直接将其输出到我的电脑桌面。为了将其编译为Java字节码,我们需要使用JDK自带的工具类JavaCompiler
进行处理,这是第一步处理。
第二步处理,通过JavaCompiler编译完成后需要通过ClassLoader将生成的字节码加载到内存中,再通过反射获取DriverTimeProxy
实例,其完整的处理流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 JavaFile javaFile = JavaFile.builder("com.youngfeng.designmode.proxy" , driverTimeProxy).build(); File sourceFile = new File (PATH);javaFile.writeTo(sourceFile); javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null , null , null ); Iterable iterable = fileManager.getJavaFileObjects(PATH + "/com/youngfeng/designmode/proxy/DriverTimeProxy.java" ); javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null , fileManager, null , null , null , iterable); task .call ();fileManager.close(); URL[] urls = new URL[] {new URL("file:" + PATH)}; URLClassLoader classLoader = new URLClassLoader(urls); Class cls = classLoader.loadClass("com.youngfeng.designmode.proxy.DriverTimeProxy" );Constructor constr = cls.getConstructor(Drivable.class ); Object obj = constr.newInstance(drivable);
结合上面的分析,Proxy类的完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class Proxy { private static String PATH = "/Users/ouyangfeng/Desktop" ; public static Object newProxyInstance(Drivable drivable) { try { MethodSpec drive = MethodSpec.methodBuilder("drive" ) .addModifiers(Modifier.PUBLIC ) .addAnnotation(Override.class ) .addException(InterruptedException.class ) .addStatement("long start = $T.currentTimeMillis()" , System.class ) .addStatement("drivable.drive()" ) .addStatement("long end = $T.currentTimeMillis()" , System.class ) .addStatement("$T.out.println(end - start)" , System.class ) .build(); FieldSpec fieldSpec = FieldSpec.builder(Drivable.class , "drivable" ) .addModifiers(Modifier.PRIVATE ) .build(); MethodSpec constructor = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC ) .addParameter(Drivable.class , "drivable" ) .addStatement("this.$N = $N" , "drivable" , "drivable" ) .build(); TypeSpec driverTimeProxy = TypeSpec.classBuilder("DriverTimeProxy" ) .addModifiers(Modifier.PUBLIC ) .addSuperinterface(Drivable.class ) .addMethod(drive) .addMethod(constructor) .addField(fieldSpec) .build(); JavaFile javaFile = JavaFile.builder("com.youngfeng.designmode.proxy" , driverTimeProxy).build(); File sourceFile = new File (PATH); javaFile.writeTo(sourceFile); javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null , null , null ); Iterable iterable = fileManager.getJavaFileObjects(PATH + "/com/youngfeng/designmode/proxy/DriverTimeProxy.java" ); javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null , fileManager, null , null , null , iterable); task .call (); fileManager.close(); URL[] urls = new URL[] {new URL("file:" + PATH)}; URLClassLoader classLoader = new URLClassLoader(urls); Class cls = classLoader.loadClass("com.youngfeng.designmode.proxy.DriverTimeProxy" ); Constructor constr = cls.getConstructor(Drivable.class ); Object obj = constr.newInstance(drivable); return obj; } catch (Exception e) { e.printStackTrace(); return null ; } } }
这里的整个过程相对比较复杂,为了让大家更直观地看到整个过程,我用一张图来描述一下整个过程到底发生了什么。
在上图中,大家可以看到,有三个小助手在帮助我们完成整个过程。他们分别是JavaPoet,帮助生成Java源码文件;JavaCompiler,帮助编译Java源码文件;Reflect(反射),帮助动态创建DriverTimeProxy实例。
至此,我们终于完成了DriverTimeProxy动态实例的创建,从现在开始,DriverTimeProxy.java的源码文件可以从我们的工程中删除掉了。换句话说,我们完成了前面需求中的其中一个,不需要新增额外的类这个部分。
等等,是否任意实现了Drivable
接口的类都可以通过Proxy.newProxyInstance()
方法创建代理实例增加日志打印功能呢?
是的,没错,你已经做到了。因为newProxyInstance的参数是Drivable接口类型,任意实现了该接口的对象都可以作为参数传入进来。
万里长征我们似乎已经走了一大半了。但,还不够!说好的实现任意自定义操作呢,这里只不过是打印日志而已。别急,我们继续往下看。
为了实现任意的自定义操作,我们需要增加进一步抽象。既然操作是任意的,那么这个操作部分就不能由我们做主,应该交给用户。可问题是,到底应该如何只交出自定义操作权限,其它交给Proxy类实现呢。
这里比较容易想到的一个思路是,增加一个统一的接口,接口中只需要包含一个方法,每次调用代理类接口方法的时候都去调用该方法。至于在该方法中到底要做什么,交给用户去处理。
这个思路是完全可行的,我们还是用一张图来描述整个过程。
上图中的InvocatinHandler
是计划用来拦截代理类的drive
方法调用的。当调用drive方法的时候实际上是调用InvocationHandler的invoke方法。
这样做的好处是什么呢,很明显,我们已经交出了自定义逻辑的主动权,用户可以在invoke方法中实现任意的自定义操作。为了能够实现自定义操作,这里的InvocationHandler
也必须是一个接口。
按照这个设计,InvocationHandler实例必须作为参数传入到newInstanceProxy方法中。为了让大家看的更清晰,我们先手动实现新版本的DriverTimeProxy
类。
我想,大家首先能够想到的第一个版本应该是这样:
1 2 3 public interface InvocationHandler { void invoke(Object proxy); }
1 2 3 4 5 6 7 8 9 10 11 12 public class DriverTimeProxy implements Drivable { private InvocationHandler invocationHandler; public DriverTimeProxy (InvocationHandler invocationHandler) { this .invocationHandler = invocationHandler; } @Override public void drive () throws InterruptedException { invocationHandler.invoke(this ); } }
这样的设计确实存在一些问题,我们先不管,先按照这个设计,使用JavaPoet
帮助我们生成这样的代码。这个时候,newProxyInstance
应该修改为下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public static Object newProxyInstance(InvocationHandler handler, Class ints) { try { MethodSpec drive = MethodSpec.methodBuilder("drive" ) .addModifiers(Modifier.PUBLIC ) .addAnnotation(Override.class ) .addException(InterruptedException.class ) .addStatement("invocationHandler.invoke(this)" ) .build(); FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class , "invocationHandler" ) .addModifiers(Modifier.PRIVATE ) .build(); MethodSpec constructor = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC ) .addParameter(InvocationHandler.class , "invocationHandler" ) .addStatement("this.$N = $N" , "invocationHandler" , "invocationHandler" ) .build(); TypeSpec driverTimeProxy = TypeSpec.classBuilder("DriverTimeProxy" ) .addModifiers(Modifier.PUBLIC ) .addSuperinterface(ints) .addMethod(drive) .addMethod(constructor) .addField(fieldSpec) .build(); JavaFile javaFile = JavaFile.builder("com.youngfeng.designmode.proxy" , driverTimeProxy).build(); File sourceFile = new File (PATH); javaFile.writeTo(sourceFile); javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null , null , null ); Iterable iterable = fileManager.getJavaFileObjects(PATH + "com/youngfeng/designmode/proxy/DriverTimeProxy.java" ); javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null , fileManager, null , null , null , iterable); task .call (); fileManager.close(); URL[] urls = new URL[] {new URL("file:" + PATH)}; URLClassLoader classLoader = new URLClassLoader(urls); Class cls = classLoader.loadClass("com.youngfeng.designmode.proxy.DriverTimeProxy" ); Constructor constr = cls.getConstructor(InvocationHandler.class ); Object obj = constr.newInstance(handler); return obj; } catch (Exception e) { e.printStackTrace(); return null ; } }
这里我们的DriverTimeProxy构造函数的参数换成了InvocationHandler实例,Drivable实例参数也就不再需要了,但必须传入代理类需要实现的接口类型。因此,newProxyInstance方法的参数变成了两个:InvocationHandler实例与接口的Class类型。
这个版本的newProxyInstance与上一个版本到底有什么不同呢。这里,我们先来做一个简单的总结。
在这个版本的实现中,我们不再需要传入真正的被代理类实例了。我们也去掉了获取方法执行时间的逻辑,这就意味着我们可以在方法的执行前后添加任意的操作了。
这样说起来可能有点抽象,我们继续回到打印方法时间的问题,在这个版本的实现中,如果要打印方法的执行时间,需要怎么做呢。看下面,我们可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class MyInvocationHandler implements InvocationHandler { private Driver target ; public MyInvocationHandler (Driver target ) { this .target = target ; } @Override public void invoke (Object proxy) { long start = System.currentTimeMillis(); try { target .drive(); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println(end - start); } } public static void main (String[] args) throws InterruptedException { Driver driver = new Driver(); Object proxy = Proxy.newProxyInstance(new MyInvocationHandler(driver), Drivable.class); ((Drivable)proxy).drive(); }
很神奇,是吗?我们最终将自定义逻辑的实现转移到了接口InvocationHandler的实现类中,至于要对方法做日志打印还是统计时间,只需要自己实现即可,例如,如果要打印日志,在invoke方法中,增加日志打印语句即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void invoke(Object proxy) { long start = System.currentTimeMillis(); System.out.println ("Drive start..." ); try { target.drive(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println ("Drive complete..." ); long end = System.currentTimeMillis(); System.out.println (end - start); }
有人说,如果调换日志打印与时间打印的顺序呢,很简单,直接修改invoke方法中的打印顺序即可,逻辑的设计已经完全掌握在了你自己手中,想怎么操作就怎么操作。
至此,经过艰难的长途跋涉,我们终于完全地抛弃掉了DriverTimeProxy
与DriverLogProxy
两个类,仅需要实现InvocationHandler接口就可以完成各种逻辑的排列组合了。而且,真正的代理类实现对用户来说是完全不可见的,这就是所谓的动态代理 。
但,还不够!如果这里我们需要实现另外一个类型的类代理,而这个类中的方法存在参数和返回值的话,这里的设计又出现问题了。为什么呢,看下面的代码:
1 2 3 4 public interface ICreator { Person.Builder create (String name ) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class Person { private String name; private int age; private Person (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } public static class Builder { private String name; private int age; public Builder (String name) { this .name = name; } public Builder age (int age) { this .age = age; return this ; } public Person build () { return new Person(this .name, this .age); } } }
1 2 3 4 5 6 7 8 public class BuilderCreator implements ICreator { @Override public Person.Builder create(String name) { return new Person .Builder(name); } }
这里我们增加了三个类,一个接口,这里需要被代理的类是BuilderCreator
。
与之前的Drivable
接口不同的是,这里的ICreator
接口中的方法不仅存在参数,而且还有返回值。那么,问题来了!
这里暴露出了我们之前设计存在的两个问题:
第一个问题:参数无法传入到InvocationHandler中。
第二个问题:无法获取到目标代理实例方法调用的返回值。
先来看第一个问题,假设我们自己实现了InvocationHandler,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyInvocationHandler implements InvocationHandler { private BuilderCreator target ; public MyInvocationHandler (BuilderCreator target ) { this .target = target ; } @Override public void invoke (Object proxy) { this .target .create(xxx) } }
在上面的代码中,我们发现,当我们对目标代理对象调用create方法的时候,发现create方法的参数无法传入进来了。
怎么办呢!为了保证方法参数可以被传入进来,这里的invoke方法必须再增加一个参数表示外部传入的参数值。由于方法参数可能有多个,这里的参数类型我们用一个对象数组来表示。
这是第一个问题,再来看第二个问题。
由于我们的create方法实际会返回BuilderCreator
实例,而在我们的设计中invoke方法是无返回值的。因此,当我们调用代理类的接口方法时,将无法获取到create方法的返回值,也就无法实现后面的调用。因此,为了可以获取到方法的返回值,这里的invoke方法还需要增加一个返回值,类型未知,我们就用Object类型。
按照上面的修正,我们的InvocationHandler接口应该这样设计:
1 2 3 public interface InvocationHandler { Object invoke(Object proxy, Object[] args); }
按照这样的设计,newProxyInstance方法应该这样修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 public static Object newProxyInstance(InvocationHandler handler, Class ints) { try { FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class , "invocationHandler" ) .addModifiers(Modifier.PRIVATE ) .build(); MethodSpec constructor = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC ) .addParameter(InvocationHandler.class , "invocationHandler" ) .addStatement("this.$N = $N" , "invocationHandler" , "invocationHandler" ) .build(); TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("DriverTimeProxy" ) .addModifiers(Modifier.PUBLIC ) .addSuperinterface(ints) .addMethod(constructor) .addField(fieldSpec); Method[] methods = ints.getMethods(); for (Method method: methods) { MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(method.getName()) .addModifiers(Modifier.PUBLIC ) .addAnnotation(Override.class ); Class [] params = method.getParameterTypes(); String args = params.length <= 0 ? null : "new Object[] {" ; for (int i = 0 ; i < params.length; i++) { Class param = params[i]; methodSpecBuilder.addParameter(ParameterSpec.builder(param, "p" + i).build()); args += "p" + i + "," ; } args = args == null ? args : args.substring(0 , args.length() - 1 ) + "}" ; Class [] exceptions = method.getExceptionTypes(); for (int i = 0 ; i < exceptions.length; i++) { methodSpecBuilder.addException(exceptions[i]); } Class returnType = method.getReturnType(); methodSpecBuilder.returns(returnType); if (returnType.getName().equals("void" )) { methodSpecBuilder.addStatement("this.invocationHandler.invoke(this, " + args + ")" ); } else { methodSpecBuilder.addStatement("Object result = this.invocationHandler.invoke(this, " + args + ")" ) .addCode("if (result instanceof $T) {\n" , TypeName.get(method.getReturnType()).box()) .addStatement("\treturn ($T) result" , TypeName.get(returnType).box()) .addCode("} else {\n" ) .addStatement("\treturn ($T) null" , TypeName.get(returnType).box()) .addCode("}\n" ); } typeSpecBuilder.addMethod(methodSpecBuilder.build()); } JavaFile javaFile = JavaFile.builder("com.youngfeng.designmode.proxy" , typeSpecBuilder.build()).build(); File sourceFile = new File (PATH); javaFile.writeTo(sourceFile); javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null , null , null ); Iterable iterable = fileManager.getJavaFileObjects(PATH + "com/youngfeng/designmode/proxy/DriverTimeProxy.java" ); javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null , fileManager, null , null , null , iterable); task .call (); fileManager.close(); URL[] urls = new URL[] {new URL("file:" + PATH)}; URLClassLoader classLoader = new URLClassLoader(urls); Class cls = classLoader.loadClass("com.youngfeng.designmode.proxy.DriverTimeProxy" ); Constructor constr = cls.getConstructor(InvocationHandler.class ); Object obj = constr.newInstance(handler); return obj; } catch (Exception e) { e.printStackTrace(); return null ; } }
newProxyInstance方法似乎又复杂了许多,为了可以获取到用户传入的参数,我们必须严格匹配参数的命名。因此,这里将参数统一命名为pN 。由于每个接口可能有多个方法,这里我们修改为通过遍历的方式获取传入接口的所有方法。同时,为了保证返回值类型与所需类型一致,在代码中,我们增加了类型判断,自动转换到预期的数据类型。
接下来,我们简单测试一下方法是否按照我们预期的情况运行。实现InvocationHandler
接口,在main方法中添加如下测试代码,尝试运行,查看结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MyInvocationHandler implements InvocationHandler { private BuilderCreator target ; public MyInvocationHandler (BuilderCreator target ) { this .target = target ; } @Override public Object invoke (Object proxy, Object[] args) { return this .target .create((String) args[0 ]); } } public static void main (String[] args) { BuilderCreator builderCreator = new BuilderCreator(); MyInvocationHandler handler = new MyInvocationHandler(builderCreator); Object proxy = Proxy.newProxyInstance(handler , ICreator.class); Person person = ((ICreator)proxy).create("Scott" ).age(18 ).build(); System.out.println(person.getAge()); }
运行上面的代码,输出结果18,很显然,代码按照预期的结果输出了。
但是,我们还是忽略了一个问题,是什么问题呢,这里先卖个关子,我们先来回顾一下每一次的方法调用过程。
在上面这张流程图中,在第一步调用中,我增加了几个方法,因为一个接口可能存在多个方法。通过newProxyInstance的处理,这些方法的调用最终都会通过调用InvocationHandler的invoke方法来实现间接调用。
所以,这里的invoke方法的调用次数与接口的方法数是一致的。如果某个接口有5个方法,这里就会调用5次。而这个时候,我们之前设计的问题也就出现了。
什么问题呢,通常来说,我们需要针对不同的方法进行不同的处理。而用户在invoke方法中无法知道当前究竟调用的是哪个方法,也就无法在invoke方法中针对不同的方法调用进行不同的处理。
这样说起来还是有点抽象,为了让大家看的更直观,我们在ICreator接口中再增加一个方法foo, 运行,查看生成的代码是什么。
1 2 3 4 5 public interface ICreator { Person.Builder create(String name); int foo(int x, int y); }
最终动态生成的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.youngfeng.designmode.proxy;import com.youngfeng.designmode.proxy.juhe.InvocationHandler;import java.lang.Integer;import java.lang.Override;import java.lang.String;public class DriverTimeProxy implements ICreator { private InvocationHandler invocationHandler; public DriverTimeProxy (InvocationHandler invocationHandler) { this .invocationHandler = invocationHandler; } @Override public Person.Builder create (String p0) { Object result = this .invocationHandler.invoke(this , new Object[] {p0}); if (result instanceof Person.Builder) { return (Person.Builder) result; } else { return (Person.Builder) null ; } } @Override public int foo (int p0, int p1) { Object result = this .invocationHandler.invoke(this , new Object[] {p0,p1}); if (result instanceof Integer) { return (Integer) result; } else { return (Integer) null ; } } }
可以看到每次调用实际调用的都是InvocationHandler的invoke方法。这个地方会让用户产生疑惑,究竟调用这个方法是发生在调用create还是foo的时候呢。
为了让大家看的更清晰,我们还是看一眼用户端需要实现的InvocationHandler接口类,已经明白的同学可以跳过这个部分继续往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MyInvocationHandler implements InvocationHandler { private BuilderCreator target ; public MyInvocationHandler (BuilderCreator target ) { this .target = target ; } @Override public Object invoke (Object proxy, Object[] args) { return this .target .create((String) args[0 ]); } }
为了让用户知道这一次的调用究竟是调用哪个方法产生的,invoke方法还需要再增加一个参数,这个参数必须代表当前调用的方法。这里需要用到反射了,我们可以通过反射拿到当前调用方法的Method实例并传入到invoke方法中。
我们摘取其中一个方法create来描述我们大概需要怎么做,看下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public Person.Builder create(String p0) { try { Method method = Drivable.class .getMethod("create" , String.class ); Object result = this .invocationHandler.invoke(this , method, new Object[]{p0}); if (result instanceof Person.Builder) { return (Person.Builder) result; } else { return (Person.Builder) null ; } } catch (Exception e) { e.printStackTrace(); return null ; } }
为了生成上面这样的代码,我们继续改进newProxyInstance方法,改进后的方法如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 public static Object newProxyInstance(InvocationHandler handler, Class ints) { try { FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class , "invocationHandler" ) .addModifiers(Modifier.PRIVATE ) .build(); MethodSpec constructor = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC ) .addParameter(InvocationHandler.class , "invocationHandler" ) .addStatement("this.$N = $N" , "invocationHandler" , "invocationHandler" ) .build(); TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("DriverTimeProxy" ) .addModifiers(Modifier.PUBLIC ) .addSuperinterface(ints) .addMethod(constructor) .addField(fieldSpec); Method[] methods = ints.getMethods(); for (Method method: methods) { MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(method.getName()) .addModifiers(Modifier.PUBLIC ) .addAnnotation(Override.class ); Class [] params = method.getParameterTypes(); String args = params.length <= 0 ? null : "new Object[] {" ; String argTypes = params.length <= 0 ? null : "new Class[] {" ; for (int i = 0 ; i < params.length; i++) { Class param = params[i]; methodSpecBuilder.addParameter(ParameterSpec.builder(param, "p" + i).build()); args += "p" + i + "," ; argTypes += TypeName.get(param).box().toString() + ".class," ; } args = args == null ? args : args.substring(0 , args.length() - 1 ) + "}" ; argTypes = argTypes == null ? argTypes : argTypes.substring(0 , argTypes.length() - 1 ) + "}" ; Class [] exceptions = method.getExceptionTypes(); for (int i = 0 ; i < exceptions.length; i++) { methodSpecBuilder.addException(exceptions[i]); } Class returnType = method.getReturnType(); methodSpecBuilder.returns(returnType); methodSpecBuilder.beginControlFlow("try" ); methodSpecBuilder.addStatement("$T method = getClass().getMethod($S, $N)" , Method.class , method.getName(), argTypes); if (returnType.getName().equals("void" )) { methodSpecBuilder.addStatement("this.invocationHandler.invoke(this, " + args + ")" ); } else { methodSpecBuilder.addStatement("Object result = this.invocationHandler.invoke(this, method, " + args + ")" ) .addCode("if (result instanceof $T) {\n" , TypeName.get(method.getReturnType()).box()) .addStatement("\treturn ($T) result" , TypeName.get(returnType).box()) .addCode("} else {\n" ) .addStatement("\treturn ($T) null" , TypeName.get(returnType).box()) .addCode("}\n" ); } methodSpecBuilder.nextControlFlow("catch ($T e)" , Exception.class ) .addStatement("e.printStackTrace()" ) .addStatement("return ($T) null" , TypeName.get(returnType).box()) .endControlFlow(); typeSpecBuilder.addMethod(methodSpecBuilder.build()); } JavaFile javaFile = JavaFile.builder("com.youngfeng.designmode.proxy" , typeSpecBuilder.build()).build(); File sourceFile = new File (PATH); javaFile.writeTo(sourceFile); javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null , null , null ); Iterable iterable = fileManager.getJavaFileObjects(PATH + "com/youngfeng/designmode/proxy/DriverTimeProxy.java" ); javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null , fileManager, null , null , null , iterable); task .call (); fileManager.close(); URL[] urls = new URL[] {new URL("file:" + PATH)}; URLClassLoader classLoader = new URLClassLoader(urls); Class cls = classLoader.loadClass("com.youngfeng.designmode.proxy.DriverTimeProxy" ); Constructor constr = cls.getConstructor(InvocationHandler.class ); Object obj = constr.newInstance(handler); return obj; } catch (Exception e) { e.printStackTrace(); return null ; } }
至此,如果我们需要对不同的方法进行不同的处理,这里就可以通过method参数进行判断了。
我们继续以代理BuilderCreator类为例,如果我们要在create与foo方法前后分别打印当前方法被调用的日志,可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MyInvocationHandler implements InvocationHandler { private BuilderCreator target; public MyInvocationHandler(BuilderCreator target) { this.target = target; } @Override public Object invoke(Object proxy, Method method , Object [] args ) { try { // 通过method .getName ( )可以获取到方法名 System.out.println(method .getName ( ) + " invoke start..." ); Object result = method .invoke ( target, args); System.out.println(method .getName ( ) + " invoke end..." ); return result; } catch (Exception e) { e.printStackTrace(); return null ; } } }
到现在为止,我们的动态代理实现基本完整了,我们的旅程也快要结束了。终于,如果我们要在某个类的某个方法前后插入任意的逻辑,不再需要新增额外的类了(InvocationHandler实现类除外),而且添加的逻辑也可以完全自定义了,我们的目标达到了!
回顾一下我们的整个探索过程,在文章的开篇部分我们从为Driver类的drive方法增加时间打印需求开始,最终选择通过代理的方式进行处理,这样做的灵活性明显高于继承。而为了去掉额外新增的类,我们使用了动态编译的方式在运行期间帮助用户生成相应的代理类。但我们并未满足于此,为了让插入的逻辑也实现自定义,我们又对插入进行了抽象,新增了插入逻辑抽象类InvocationHandler,最终将对Driver类的代理转换到对InvocationHandler实现类的代理中。将方法调用的主动权交给用户(InvocationHandler的实现类处理)。这样,无论是多么复杂的需求,对用户来说,只需要实现InvocationHandler接口增加自定义处理即可,即使某个类有100个方法,他需要的也只是一个InvocationHandler的实现类而已。
等等,我们似乎还忘记了一件事情。一直以来我们生成的动态代理类名称都叫做DriverTimeProxy
,而此刻它的功能已经不再仅仅是为Driver
类增加时间打印而已了。因此,这里我们将最终生成的代理类名称修改为Proxy$0
。最终版本的实现大家可以查看文章的附录部分,点击下方链接前去查看。
动态代理到底有什么用 在上面的整个过程中,我们可谓是经历了千难万险,终于完成了一个简易版本的动态代理。那么动态代理到底有什么作用呢。为了让大家直观地感受到,动态代理到底可以做什么,我们先一起来看一个简单的例子。
这里我们创建两个类来简单模拟数据库事务的提交过程(以下代码仅作为演示使用,不具有实际使用价值):
1 2 3 public interface TransactionConstr { void commit(int x); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Database implements TransactionConstr { public static Database getInstance () { return new Database(); } @Override public void commit (int x) { if (x < 0 ) throw new IllegalArgumentException(); System.out.println("Transaction commit success..." ); } public void rollback () { System.out.println("Transaction rollback..." ); } }
为了防止出现脏数据,我们需要在事务提交失败的时候将数据回滚。在没有动态代理之前,我们会这样处理:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { Database db = Database.getInstance(); try { db.commit(-1 ); } catch (Exception e) { e.printStackTrace(); db.rollback(); } }
而使用动态代理,我们这样做:实现InvocationHandler,代理Database类,在invoke方法中捕获方法可能抛出的异常,一旦发现异常就调用rollback方法自动回滚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class AutoRollbackInvocationHandler implements InvocationHandler { private Database target ; public AutoRollbackInvocationHandler (Database target ) { this .target = target ; } @Override public Object invoke (Object proxy, Method method, Object[] args) { try { method.invoke(target , args); } catch (Exception e) { target .rollback(); } return null ; } }
这里一定还是会有人说,这两种调用方式貌似没有什么区别啊。第二种写法在每次调用的时候不也需要先使用newProxyInstance生成代理类,然后再调用吗,这样反而麻烦了许多。
其实,这两种方式有一个本质上的区别,前者免不了每次都添加try-catch
语句。而后者由于是使用模板化的实现,我们可以在框架层帮助用户自动生成动态代理类,自动添加try-catch
语句,这样用户就可以使用非常简单的方式实现事务的自动回滚了。例如,在方法上面添加一个自动回滚的注解,框架层识别注解自动生成代理类实例。这样,你需要的仅仅是一个注解而已了。如果你了解Java后端开发,你应该就知道我在说什么了。
以上是动态代理设计模式的一个经典应用场景,但实际上,动态代理的使用场景还有很多。仔细观察动态代理的执行过程,它似乎为我们开辟了一个新的编程方式。相对于传统的流线型编程方式,动态代理可以在任意已经实现的类中的任意方法中插入自定义的逻辑,就像一把刀一样,将代码的执行过程切成片段,再往里插入自定义的逻辑。至此,计算机的编程方式就变成了cut-insert (切开-插入)的方式进行。这种编程方式,在计算机科学中,被称之为面向切面编程(AOP) 。
AOP 动态代理是面向切面编程的一种经典实现。在大部分框架中,实现AOP都会使用JDK自带的动态代理处理。当然,JDK的实现其实也有一定的局限性,这就是为什么CGLIB这样的动态代理库大行其道的原因。不过,这不是本文讨论的重点,大家如果对CGLIB感兴趣的话,可以在文章下方给留言。
说回AOP,到底什么是面向切面编程呢?前面其实已经有了一个简单的解释了,为了让大家看的更直观,我们先来看一张图:
以上是面向切面编程框架AspectJ 的示例图,AspectJ是一个非常流行面向切面编程Java库。我们就用这张图来给大家讲一下相对于传统编程,面向切面编程到底有什么不同。
图中绿色箭头表示正常代码的执行过程,PointCut 表示切入点,即在哪个类的哪些位置插入自定义逻辑。Join Points 表示连接点,即具体的插入位置,例如方法调用前,调用后,异常抛出等等。
由此可见,相当于传统的编程模型,AOP的思考点是:找到匹配的切入点,插入自定义逻辑,而且这种插入对原有框架代码是无侵入性的。相对于面向对象编程模型使用继承的方式进行扩展这种侵入性的处理方式,显然是一个巨大的进步。
正是因为AOP具备高度自由、无侵入性的这些特点,才使得它在Spring等知名开源框架中有着大量的应用,而这一切的核心都依赖于AOP最经典的实现方式:动态代理 。我想,这应该是动态代理到底有什么用这个问题最好的回答。
JDK实现揭秘 实际上,在上面的整个探索过程中,我们都是参照JDK实现来进行讲解的。但还有哪些地方是我们考虑不周的呢,一起来看看JDK实现吧。
打开JavaSE官方文档,找到Proxy与InvocationHandler类。
Proxy.java
InvocationHandler
可以看到,JDK版本的newProxyInstance方法中一个有三个参数,第一个参数是ClassLoader,这就意味着我们可以指定自己的类加载器。第二个参数是接口数组,这是因为需要被代理的接口可能不止一个,或者说代理类实现的接口可能不止一个。这是在我们的版本中没有考虑到的一点,实际使用场景中这种情况其实是很常见的。
JDK版本中,InvocationHandler 接口的定义与我们的版本是完全一致的。在前一篇文章中,关于这个接口中方法里面几个参数的意思,有不少同学问到,甚至有同学在微信公众号“欧阳锋工作室”给我私信,问到了这个问题。这里统一给大家解答一下:
proxy :这个参数表示动态生成的代理类实例,在某些场景中你可能需要对代理实例做一些特殊的处理,这个时候,这个参数的作用就出来了,大多数情况下你不需要用到这个参数。
method :这个参数在前面的文章中其实已经讲过了,它表示实际调用的代理类的接口方法的Method实例,用户可以使用它调用目标代理类的方法。
args :这个参数表示method对应方法传入的参数值,这里可以提供给method方法反射调用,也可以通过直接调用的方式逐一传入参数值到目标代理类方法中。
返回值 :invoke方法的返回值,这里也有一些同学问到,这个也是上一篇文章中解释不够到位的地方。这一次我们在前面的例子中详细解释了invoke方法的返回值到底有什么作用。它实际上对应的是被代理类对应method方法的返回值。这是与接口方法一一对应的,方便调用者轻松获取到实际代理类方法调用的返回值。
写在最后 最后,感谢大家陪伴我走过了这一段艰难的探索旅程,这的确不太容易。如果你完整地看完了整篇文章,并且根据文章的推进过程同步完成了代码开发,应该给自己鼓个掌。因为,这的确不太容易。Java动态代理设计模式是所有设计模式中最难理解的一个。如果你已经看懂了这个设计模式,其它的设计模式就已经是“除却巫山不是云”了。
希望这篇文章说清楚了前一篇文章大家提的每一个问题,也说清楚了动态代理的每一处细节。如果你还有疑问,欢迎在文章下方给我留言,或者来我的微信公众号“欧阳锋工作室”给我发私信。
附录 例子源码:https://github.com/yuanhoujun/java-dynamic-proxy
上一篇:点这里前往
关注微信公众号”欧阳锋工作室“,阅读更多文章。