什么是代理?
怎么实现代理模式?
代理模式有什么实际用途?
看一个简单的例子:
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)); } |
这个方案看起来似乎没有问题,但实际这个方案忽略了准备这些方法所需要的时间,执行一个方法需要开辟栈内存、压栈、出栈等操作,这部分时间也是不可以忽略的。因此,这个解决方案不可行。那么,还有什么方案可以做到呢?
- 使用继承
继承是最直观的解决方案,相信你已经想到了。最开始我想到的解决方案就是继承。我可以重新创建一个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)); } } |
同样的,通过这种方法我们也可以获得小鸟的飞行时间。那这两种方法孰优孰劣?
继续深入思考,用问题推到来解答这个问题:
问题一:如果我们还需要在fly()方法前后打印日志,记录飞行开始和飞行结束,怎么办?
有人说,很简单!继承Bird2并在前后添加打印语句即可。那么问题来了,请看问题二。
public class Bird2 extends Bird {
|
问题二:如果我需要调换执行顺序,先打印日志,再获取飞行时间,怎么办?
有人说,在新建一个类Bird4继承Bird,打印日志。再新建一个类Bird5继承Bird4,获取方法执行时间。
问题显而易见:使用继承将导致类无限制扩展,同时灵活性也无法获得保证。那么使用“聚合”是否可以避免这个问题呢?答案是可以!但我们的类要稍微改造一下。需要修改Bird3类,将聚合对象Bird类型修改为Flyable。为了更加清楚,我将新建一个BirdTimeProxy,用于获取方法执行时间的代理。同时我们新建BirdLogProxy代理类用于打印日志。
public class BirdTimeProxy implements Flyable {
|
public class BirdLogProxy implements Flyable { |
如果我们需要先记录日志,再获取飞行时间,可以在调用地方这么做:
public static void main(String[] args) { Bird bird = new Bird(); BirdLogProxy p1 = new BirdLogProxy(bird); BirdTimeProxy p2 = new BirdTimeProxy(p1);
p2.fly(); } |
输出结果:
Bird fly start... Bird is flying... Bird fly end... Fly time = 65 |
反过来,可以这么做:
public static void main(String[] args) { Bird bird = new Bird(); BirdTimeProxy p2 = new BirdTimeProxy(bird); BirdLogProxy p1 = new BirdLogProxy(p2);
p1.fly(); } |
输出结果:
Bird fly start... Bird is flying... Fly time = 935 Bird fly end... |
实现原理:
静态代理:
上面BirdTimeProxy类,它的fly方法中我们直接调用了flyable->fly()方法。其实BirdTimeProxy就是代理了传入的Flyable对象,这就是典型的静态代理实现。
从表面上看,静态代理已经完美解决了我们的问题。可是,试想一下,如果我们需要计算SDK中的100个方法的运行时间。同样的代码至少需要重复100次,并且创建至少100个代理类。假设Bird类有多个方法,我们需要知道其他方法的运行时间,同样的代码也至少需要重复多次。因此,静态代理至少有以下两个局限性问题:
-
- 如果同时代理多个类,依然会导致类无限制扩展。
- 如果类中有多个方法,同样的逻辑需要反复实现。
那么是否可用使用同一个代理类来代理任意对象呢?已获取方法运行的时间为例,是否可以使用统一类(例如:TimeProxy)来计算任意对象的任意方法的执行时间呢?甚至代理的逻辑也可以自定指定。比如,获取方法的执行时间,打印日志,这类逻辑可以自己指定。使用“动态代理”可以实现。
动态代理
是否可以使用同一类,例如TimeProxy来计算任意对象的任一方法的执行时间呢?
我想,你第一个想到的解决方案应该是反射。反射是用于获取已创建实例的方法或者属性,并对其进行调用或者赋值。很明显,这里,反射解决不了问题。不过,如果我们可以动态生成TimeProxy这个类,并且动态编译。然后,再通过反射创建对象并加载到内存中,不就实现对任意对象进行代理了吗?
动态生成Java源文件是一个复杂的过程。我们使用JavaPoet第三方库帮我们生成第一步:TimeProxy源码。
public class Proxy {
public static Object newProxyInstance() throws IOException { TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy").addModifiers(Modifier.PUBLIC).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("C:\\Users\\jiangwei121\\Desktop")); return null; }
} |
第二步:编译TimeProxy源码
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 = "C:\\Users\\jiangwei121\\Desktop";
|
第三步:加载到内存中并创建对象:
File file = new File("C:\\Users\\jiangwei121\\Desktop\\");
URL url =file.toURI().normalize().toURL();
URL[] urls = new URL[] {url};
URLClassLoader classLoader = new URLClassLoader(urls,Thread.currentThread().getContextClassLoader());
Class clazz = classLoader.loadClass("com.alex.proxy.TimeProxy");
Constructor constructor = clazz.getConstructor(Flyable.class);
Flyable flyable = (Flyable) constructor.newInstance(new Bird());
flyable.fly(); |
执行结果:
Bird is flying... Fly Time = 422 |
三个步骤,我们解决了两个问题:
- 不需要手动创建TimeProxy
- 可以代理任意实现了Flyable接口的类对象,并获取接口方法的执行时间
下面解决对任意对象的代理。
第四步:增加InvocationHandler接口
查看Proxy-> newProxyInstance()的源码,代理类继承的接口我们是写死的,为了增加灵活性,我们将接口类型作为参数传入:
接口的灵活性问题解决了,TimeProxy的局限性依然存在,它只能用于获取方法的执行时间,而如果要在方法执行前后打印日志则需要重新创建一个代理类,这肯定是不行的!
为了增加代理的灵活性,我们需要考虑将代理的处理逻辑剥离出来,也就是打印方法执行时间。新增InvocationHandler接口,用于处理自定义逻辑:
public interface InvocationHandler { void invoke(Object proxy, Method method,Object[] args); } |
这样修改后,如果其他人希望对代理类进行自定义处理。只需要实现该接口,并在invoke方法中进行相应的处理即可。这里的三个参数是为了与JDK源码保持一致:
- proxy=>动态生成代理类,这里是TimeProxy
- method=>传入接口的所有Method对象
- arge=>当前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 class Proxy {
|
在main方法中测试newProxyInstance查看生成的TimeProxy源码:
Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird())); |
生成的TimeProxy.java源码
package com.alex.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.alex.proxy.Flyable.class.getMethod("fly"); this.handler.invoke(this, method, null); } catch(Exception e) { e.printStackTrace(); } } } |
MyInvocationHandler.java
public class MyInvocationHandler implements InvocationHandler{ |
至此,整个方法的调用栈变成了这样:
静态代理部分,我们在代理类中传入了被代理对象。可是,使用了newProxyInstance生成动态代理对象的时候,我们居然不再需要传入被代理对象了。我们传入了实际对象是InvocationHandler实现类的实例,这看起来像生成了InvocationHandler的代理对象,在动态生成的代理类的任意方法中都会间接调用InvocationHandler->invoke(proxy,method,args)方法。
其实的确是这样。TimeProxy真正的代理对象就是InvocationHandler,不过这里设计巧妙之处在于,InvocationHandler是一个接口,真正的实现由用户指定。另外,在每一个方法执行的时候,invoke方法都会被调用,这个时候可以进行自定义处理,根据method的特征信息进行判断分别处理。
如何使用
上面的这些都是再详细解释Proxy->newProxyInstance方法执行时候真正发生的事情,而在实际使用过程中其实完全可以忽略这些。直接按照设计者的思路,进行使用。:
- Proxy->newProxyInstance(inf,handler)用于生成代理对象
- InvocationHandler:这个接口主要用于自定义代理逻辑处理
- 为了完成对被代理对象的方法拦截,我们需要在InvocationHandler对象中传入被代理对象实例。
上面的这些代码,你可以看到我将Bird实例传入到了MyInvocationHandler中,就是这三点原因。
这样设计有什么好处呢?到底为什么饶了一大圈,变成了这样样子,到底是图什么?
想想,到此为止。如果我们还需要对其他任意对象进行代理,是否还需要改动newProxyInstance方法的源代码?答案是:完全不需要!
只要在调用newProxyInstance方法中指定代理需要实现的接口,指定用于自定义处理的InvocationHandler接口的实现类,整个代理逻辑都在自定义的InvocationHandler实现类中进行处理。到此为止,我们终于可以从不断写代理类用于实现自定义逻辑的重复工作中解放出来了。从此需要做什么,交给InvocationHandler。
事实上,我们之前给自己定下的目标“使用同一个类来计算任意对象的任意方法的执行时间”已经实现了。严格来说,我们超儿完成了任务,TimeProxy不仅可以计算方法执行时间,也可以打印方法执行日志,这完全取决于你的InvocationHandler接口实现。因此,这里取名为TimeProxy其实已经不合适了。我们可以修改为和JDK命名一致,即(java.lang.reflect) Proxy()。
JDK实现揭秘
通过上面的这些步骤,我们完成了一个简易的仿JDK实现的动态代理。我们比较一下,看看JDK动态代理的实现和我们有什么不同。
Proxy.java
InvocationHandler.java
官方版本Proxy类提供的方法多一些,主要使用的接口newProxyInstance参数也和我们的不太一样。
- Classloader:类加载器,可以使用自定义的类加载器。我们的实现为了简单,直接在代码里写死了Classloader。
- Class<?>[]:第二个参数JDK允许我们自己实现的代理类同时实现多个接口。我们只允许传一个接口,快速实现业务逻辑。
- 最后一个参数,和我们的完全一样。
但这里有一个小细节,官方版本的invoke方法有返回值。我们的版本是没有返回值的。
这里的返回值类型必须和传入接口的返回值类型一致,或者与其封装对象的类型一致。JDK的版本可以允许接口有返回值。我们的版本中,Flyable接口的所有方法都是没有返回值的。
答疑解惑
invoke方法的第一个参数proxy到底有什么作用?
这个问题其实也好理解,如果你的接口中有方法需要返回自身,如果在invoke中没有传入这个参数,将导致实例无法正常返回。在这种场景中,proxy的用途就表现出来了。简单来说,这其实就是最近非常火的链式编程的一种应用实现。
动态代理到底有什么用?
学习任何一门技术,一定要问一问自己,这到底有什么用。其实,在这篇文章的讲解过程中,我们已经说出了它的主要用途。你发现没,使用动态代理我们居然可以在不改变源码的情况下,直接在方法中插入自定义逻辑。这有点不太符合我们的一条线走到底的编程逻辑,这种编程模型有一个专业名称叫 AOP。所谓的AOP,就像刀一样,抓住时机,趁机插入。
基于这样一种动态特性,我们可以用它做很多事情,例如:
- 事务提交或回退(Web开发中很常见)
- 权限管理
- 自定义缓存逻辑处理
- SDK Bug修复 ...
Spring 的设计模式中就有很多动态代理。
代码:https://github.com/loveflywei/dynamic-proxy
微信:wei_wei10