代理模式简介
代理模式是设计模式中非常常用的一种设计模式,在Java和安卓源码中都有涉及。使用代理模式可以对一些事务进行日志处理,权限控制等。
为其他对象提供一种代理以控制对这个对象的访问。代理对象起到中介作用,可去掉功能服务或增加额外的服务。
代理模式的几种实现方式
代理模式一般有三种:
- 使用继承
- 使用聚合
- 动态代理(重点)
继承和聚合的方式相对于动态代理来说可以认为是静态代理 ,因为这两种方式的代理类是静态实现的,而动态代理类是根据参数的不同动态实现的。
代理的实现
继承实现代理
使用继承的方式实现代,理非常简单。其实原理就是子类继承父类,子类是父类的代理。
构建以下场景:
比如我们有一公共汽车Bus,在汽车启动前需要确认司机身份,司机身份确认后才能开始行驶,并记录汽车行驶时间,那么我们该如何构建类的关系呢?如果再有一辆小轿车也要做相同的事情呢?如果再有火车,马车。。。
最简单的方法就是:定义一个汽车类Bus.java,它有一个move方法,在move调用前进行身份验证,记录时间这些工作。再定义一个轿车类Car.java,它有一个move方法,在move方法调用前进行身份验证,记录这些工作。再定义。。。很明显,我们这样会使代码无限增多,维护性差,扩展性差。那么我们就定义一个接口Moveable.java,所有类型的车的类都要实现这个接口。
Moveable.java
public interface Moveable { void move(); }
Bus.java
public class Bus implements Moveable { @Override public void move() { //预处理逻辑 System.out.println("确认司机身份中..."); System.out.println("启动..."); long startTime = System.currentTimeMillis(); //核心逻辑 System.out.println("moving"); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } long endTime = System.currentTimeMillis(); System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒"); } }
我们实现了一Bus.java类,但足以说明问题了。
可以发现,此时我们的身份验证和时间记录是写死在move方法中的,很明显这是内聚过度,导致了move的核心逻辑和预处理(身份验证)耦合性太强。
这时我们需要把身份验证和move的核心逻辑进行解耦,新建一个代理类ExtendsProxy.java
public class ExtendsProxy extends Bus { @Override public void move() { //预处理逻辑 System.out.println("确认司机身份中..."); System.out.println("启动..."); long startTime = System.currentTimeMillis(); super.move();//调用被 代理对象的方法 long endTime = System.currentTimeMillis(); System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒"); } }
这时在Bus.java的move方法中就不需要做预处理操作了,Bus.java如下:
public class Bus implements Moveable { @Override public void move() { System.out.println("moving"); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } }
我们来测试一下:
public class Client { public static void main(String[] args) { //新建代理类对象 Moveable m = new ExtendsProxy(); //调用代理方法 m.move(); } }
结果如下:
确认司机身份中... 启动... moving 车辆行驶时间:175毫秒
非常好!很开心我们实现了一个代理。可是突然又增加了需求,我们不仅要代理Bus了,还要代理小轿车Car,那怎么办呢?有的人说:好办,再新建一个类继承Car.java,用同样的方式实现代理不就可以了吗?这样确实是可以的,但是相信你的leader会不开森,要是代理一百个类,那你还再写一百个代理 类么?这时你突然醒悟过来:继承的方式实现代理有很大的局限性,扩展性不好,那么我们是坚决摒弃这种做法的。
聚合实现代理
聚合方式是通过把被代理对象当作参数传入代理对象中。这种方式是很常用的方式。
我们实现UnionProxy.java,它也需要实现Moveable接口。为什么要实现Moveable接口呢?统一接口,统一调用。
public class UnionProxy implements Moveable { private Moveable m; public UnionProxy(Moveable m) { this.m = m; } @Override public void move() { if (m != null) { //预处理逻辑 System.out.println("确认司机身份中..."); System.out.println("启动..."); long startTime = System.currentTimeMillis(); //调用被 代理对象的方法 m.move(); long endTime = System.currentTimeMillis(); System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒"); } } }
这时如果我们再想要代理其实实现了Movealbe的类,比如轿车,马车之类 的,那么我们做的是直接把它们的对象传到UnionProxy的构造参数中即可,解决了继承方式的弊端,真棒!
但是,但是,但是,问题又来了,现在Leader要求,先启动汽车,再确认身份,顺序颠倒了,那么怎么办呢。因为我们的预处理逻辑是硬编码的,要想添加新的逻辑,那么只能再写一个UnionProxy2.java,在move方法中重新定义预处理逻辑,这样好不好呢?没错,正如你所想的那样,肯定是不好的,因为如果我们有一万种预处理逻辑,就要再写一万个代理类吗?我们希望有一种代理类,既可以动态得传入被代理对象,又可以动态得生成代理方法,这就是动态代理!
动态代理
动态代理在JDK中提供了API,我们可以很方便得使用它,下面我们来了解一下动态代理是如何使用的吧。
JDK中在java.long.reflect中提供了两个类:
- Proxy.java 代理类,用来生成代理对象
- InvocationHandler.java 实现这个接口,添加预处理逻辑等
使用流程如下:
- 新建xxxHandler实现InvocationHandler接口,添加预处理逻辑
- 创建被代理类对象,并获取被代理类对象的ClassLoader和Intefaces
- 创建xxxHandler对象
- 调用Proxy.newInstance(loader, interfaces, handler);来创建代理类对象
- 调用代理类对象的代理方法
我们这里创建DDdogerHandler.java实现InvocationHandler接口
DDdogerHandler.java
public class DDdogerHandler implements InvocationHandler { //被代理对象 private Object target; public DDdogerHandler(Object target) { super(); this.target = target; } /** * o 被代理对象 * m 被代理的方法 * args 被代理方法的参数 */ @Override public Object invoke(Object o, Method m, Object[] args) throws Throwable { // 预处理逻辑 System.out.println("确认司机身份中..."); System.out.println("启动..."); long startTime = System.currentTimeMillis(); m.invoke(target);//调用被代理方法,并传入被代理对象作为参数 long endTime = System.currentTimeMillis(); System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒"); return null; } }
接下来创建代理对象并调用:
public static void main(String[] args) { Moveable target; // 新建被代理对象 target = new Bus(); // target = new Car(); // 新建handler对象 DDdogerHandler handler = new DDdogerHandler(target); /** * 参数: loader 类加载器 * interfaces 被代理对象实现的接口 * h InvocationHandler对象 */ Moveable m = (Moveable) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); m.move(); }
运行结果如下:
确认司机身份中... 启动... The bus is moving 车辆行驶时间:72毫秒
我们先来分析一下,如何使用JDK提供的API实现对任意对象添加任意的代理
正如之前所说:如果我们要对其它类比如Car.java实现先验证司机身份再启动的,最后记录行驶时间的代理,那我们应该怎么做呢?
因为代理的预处理工作不变,所以只需要把传入把Bus对象换成Car对象即可,如下:
public static void main(String[] args) { Moveable target; // 新建被代理对象 // target = new Bus(); target = new Car(); // 新建handler对象 DDdogerHandler handler = new DDdogerHandler(target); /** * 参数: loader 类加载器 * interfaces 被代理对象实现的接口 * h InvocationHandler对象 */ Moveable m = (Moveable) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); m.move(); }
运行结果如下:
确认司机身份中... 启动... The car is moving 车辆行驶时间:770毫秒
怎么样,很简单吧。那么我们如果想要实现先启动,再验证司机身份的预处理逻辑该怎么做呢?那我们只需要把参数传入到handler对象中,根据参数来定义预处理逻辑的顺序即可。这里不再详述。
看来动态代理确实很方便啊,那它内部究竟是如何实现的呢?
下面我们来手动实现动态代理吧~
手动实现动态代理
动态代理,顾名思义,就是动态得生成代理类,再动态得把代理类编译成class文件,最后生成代理类对象并调用的一个过程。所以我们的实现思路如下:
- 首先要声明一段源码,要知道源码其实就是一些字符,把这些字符写到文件中就是源码文件了
- 编译源码,JDK提供了相关的Complier API,生成字节码文件
- 通过反射通过字节码文件生成代理对象
PS:为了与JDK的API区别开来,以下类使用不同的命名方式。
首先要有一个代理类DDdogerProxy.java,它有一个方法newInstance()来生成代理类对象。生成对象的过程就是上面的3步。
话不多说,先上源码!
public class DDdogerProxy { /** * @param infc 被代理类实现的接口Class对象 * @return * @throws Exception */ public static Object newInstance(Class infc) throws Exception { //1 生成源码文件 //1.1 声明一段源码,这里使用的是UnionProxy的源码进行稍加整理 //其中Moveable需要根据传入的参数infc来确定 //构建的代理类名取为$Proxy0,与jdk保持一致,当然也可以换成别的名字 String sourceStr = "package com.dddoger.proxy;\r\n" + "\r\n" + "public class $Proxy0 implements "+ infc.getName() +" {\r\n" + " private "+ infc.getName() +" m;\r\n" + " public $Proxy0("+ infc.getName() +" m) {\r\n" + " this.m = m;\r\n" + " }\r\n" + " @Override\r\n" + " public void move() {\r\n" + " if (m != null) {\r\n" + " //预处理逻辑\r\n" + " System.out.println(\"确认司机身份中...\");\r\n" + " System.out.println(\"启动...\");\r\n" + " long startTime = System.currentTimeMillis();\r\n" + " \r\n" + " //调用被 代理对象的方法\r\n" + " m.move();\r\n" + " \r\n" + " long endTime = System.currentTimeMillis();\r\n" + " System.out.println(\"车辆行驶时间:\" + (endTime - startTime) + \"毫秒\");\r\n" + " }\r\n" + " }\r\n" + "}"; //1.2 定义文件名,相对路径,位于工程目录下/bin/com/dddoger/proxy/$Proxy0.java String filename = System.getProperty("user.dir") +"/bin/com/dddoger/proxy/$Proxy0.java"; File file = new File(filename); //1.3 这里使用的commons-io方便操作,生成源文件$Proxy0.java FileUtils.writeStringToFile(file, sourceStr); //2 编译源码文件 //2.1 获取系统编译器 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //2.2 通过cmopiler对象获取文件管理者对象 StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null); //2.3 获取文件对象 Iterable fileObjs = fileMgr.getJavaFileObjects(filename); //2.4 建立编译任务 CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, fileObjs); //2.5 开始编译任务 task.call(); //3 加载class文件到内存 //3.1 获取类加载器 ClassLoader loader = ClassLoader.getSystemClassLoader(); //3.2 加载指定的类到内存,我们只需要加载代理类$Proxy Class clazz = loader.loadClass("com.dddoger.proxy.$Proxy0"); //4 使用构造器创建代理对象并返回 //4.1 获取被代理类的构造器,参数为被代理类对象 Constructor constructor = clazz.getConstructor(infc); //4.2 创建代理对象,并传入参数 Object proxy = constructor.newInstance(new Bus()); return proxy; } }
注释已经写得很详细了,相信大家理解整体的思路。
然后来测试一下看:
public static void main(String[] args) throws Exception { Moveable target; //被代理对象 target = new Bus(); //生成代理对象,因为代理类是实现了Movealbe接口的, //也就是说代理类和被代理类都实现了同一接口 Moveable m = (Moveable)DDdogerProxy.newInstance(Moveable.class); m.move(); }
运行结果:
确认司机身份中... 启动... The Bus is moving 车辆行驶时间:533毫秒
再说明一下中间过程,这样更清晰一些。
在执行完FileUtils.writeStringToFile(file, sourceStr);
之后,会生成源码文件$Proxy0.java,使用Navigator可以看到:
$Proxy0.java内容如下:
package com.dddoger.proxy; public class $Proxy0 implements com.dddoger.proxy.Moveable { private com.dddoger.proxy.Moveable m; public $Proxy0(com.dddoger.proxy.Moveable m) { this.m = m; } @Override public void move() { if (m != null) { //预处理逻辑 System.out.println("确认司机身份中..."); System.out.println("启动..."); long startTime = System.currentTimeMillis(); //调用被 代理对象的方法 m.move(); long endTime = System.currentTimeMillis(); System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒"); } } }
经过执行task.call();
后会生成$Proxy0.class
字节码文件,需要先加载到内存中,再创建对象。
写到这里,动态代理还没有完全实现,因为我位的代理逻辑是硬编码的,需要把这些代理逻辑解耦,该怎么办呢?别忘了,JDK中有一个InvocationHandler的类,叫做事务处理器,代理逻辑是放在这里面的。把InvocationHandler对象传到DDdogerProxy作为参数,再反射调用InvocationHandler的方法不就可以了吗。
DDdogerInvocationHandler.java
public interface DDdogerInvocationHandler { /** * @param proxy 代理对象 * @param m 被代理方法 * @throws Exception */ void invoke(Object proxy, Method m) throws Exception; }
实现该接口,并添加代理逻辑
BusHandler.java
public class BusHandler implements DDdogerInvocationHandler { private Object target; public BusHandler(Object target) { super(); this.target = target; } @Override public void invoke(Object proxy, Method m) throws Exception { // 预处理逻辑 System.out.println("确认司机身份中..."); System.out.println("启动..."); long startTime = System.currentTimeMillis(); m.invoke(target); long endTime = System.currentTimeMillis(); System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒"); } }
我们的代理类DDdogerProxy.java也要做一些修改,添加handler参数,并且动态得生成代理逻辑。
修改后的DDdogerProxy.java如下:
public class DDdogerProxy { /** * @param infc 被代理类实现的接口Class对象 * @return * @throws Exception */ public static Object newInstance(Class infc, DDdogerInvocationHandler h) throws Exception { //1 生成源码文件 //1.1 声明一段源码,这里使用的是UnionProxy的源码进行稍加整理 //其中Moveable需要根据传入的参数infc来确定 //构建的代理类名取为$Proxy0,与jdk保持一致,当然也可以换成别的名字 String methodStr = ""; //循环获取被代理类的方法 for(Method m : infc.getMethods()) { methodStr += " @Override\r\n" + " public void " + m.getName() + "() {\r\n" + " try{\r\n" + " Method md = " + infc.getName() + ".class.getMethod(\"" + m.getName() + "\");" + " h.invoke(this, md);\r\n" + " }catch(Exception e){e.printStackTrace();}\r\n" + " }"; } String sourceStr = "package com.dddoger.proxy;\r\n" + "import java.lang.reflect.Method;\r\n" + "import com.dddoger.proxy.manual.DDdogerInvocationHandler;\r\n" + "public class $Proxy0 implements "+ infc.getName() +" {\r\n" + " private DDdogerInvocationHandler h;\r\n" + " public $Proxy0(DDdogerInvocationHandler h) {\r\n" + " this.h = h;\r\n" + " }\r\n" + methodStr + "}"; //1.2 定义文件名,相对路径,位于工程目录下/bin/com/dddoger/proxy/$Proxy0.java String filename = System.getProperty("user.dir") +"/bin/com/dddoger/proxy/$Proxy0.java"; File file = new File(filename); //1.3 这里使用的commons-io方便操作,生成源文件$Proxy0.java FileUtils.writeStringToFile(file, sourceStr); //2 编译源码文件 //2.1 获取系统编译器 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //2.2 通过cmopiler对象获取文件管理者对象 StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null); //2.3 获取文件对象 Iterable fileObjs = fileMgr.getJavaFileObjects(filename); //2.4 建立编译任务 CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, fileObjs); //2.5 开始编译任务 task.call(); //3 加载class文件到内存 //3.1 获取类加载器 ClassLoader loader = ClassLoader.getSystemClassLoader(); //3.2 加载指定的类到内存,我们只需要加载代理类$Proxy Class clazz = loader.loadClass("com.dddoger.proxy.$Proxy0"); //4 使用构造器创建代理对象并返回 //4.1 获取被代理类的构造器,参数为被代理类对象 Constructor constructor = clazz.getConstructor(DDdogerInvocationHandler.class); //4.2 创建代理对象,并传入参数 Object proxy = constructor.newInstance(h); return proxy; } }
主要修改有以下几点:
- newInstance方法中添加了事务处理器DDdogerInvocationHandler的h参数,DDdogerInvocationHandler在它的invoke方法中有预处理逻辑并反射调用了代理方法。
- 代理类$Proxy0.java要动态实现Moveable接口的所有方法,在方法内部使用反射调用事务处理器的方法。
for(Method m : infc.getMethods()) { methodStr += " @Override\r\n" + " public void " + m.getName() + "() {\r\n" + " try{\r\n" + " Method md = " + infc.getName() + ".class.getMethod(\"" + m.getName() + "\");" + " h.invoke(this, md);\r\n" + " }catch(Exception e){e.printStackTrace();}\r\n" + " }"; }
- 生成$Proxy0的对象使用的参数也要对应改变
//4.1 获取被代理类的构造器,参数为被代理类对象 Constructor constructor = clazz.getConstructor(DDdogerInvocationHandler.class); //4.2 创建代理对象,并传入参数 Object proxy = constructor.newInstance(h);
再测试一下:
target = new Bus(); BusHandler handler = new BusHandler(target); //也就是说代理类和被代理类都实现了同一接口 Moveable m = (Moveable)DDdogerProxy.newInstance(Moveable.class, handler); m.move();
输出结果:
确认司机身份中... 启动... The Bus is moving 车辆行驶时间:137毫秒
这时我们的动态代理就基本实现了,只不过我们实现的事务处理器的invoke方法少一个args参数,返回值也为void,但是不影响我们整体的,我们暂时不再过多讨论。