详述代理模式及动态代理简单实现
2017-10-20 19:24
441 查看
前言:本文章总结于马士兵老师系列教程,是根据视频中提出的问题的思维为大纲写的
注:源代码在最后,过程中的代码不一定能运行,请参看源代码
Car代码:
这个时候我们就需要用到代理模式了,Car的代码不能改但是它的代理类可以,而代理方式也有两种如下:
1. 通过继承Car来代理
另外一种方式通过聚合的代理
聚合,一个类中包含另外一个类。
用聚合的方式代理时,我们需要代理类和被代理类有同样的行为,我们可以通过实现相同的接口来实现
接口代码:
让Car类implement接口,这里就不贴代码了
代理类实现接口:
2. 以聚合的方式实现:我们可以再来一个代理实现Move接口,代码如下:
到这里可能会有疑问,这不是跟继承存在一样的问题吗?别急,请看下面一种实现:
可能会有点绕,这里写个测试类来理一理思路,测试类代码:
运行效果:
1. 先计算时间再启动
2. 先启动再计算时间
以上就实现了一个静态代理。从上面的效果可以看出实现了预期的效果,我们可以任意的指定谁先代理谁后代理,可以看作是横向扩展了。
到这里会有很多人有疑惑,这不是装饰设计模式吗?从实现语法的角度来讲确实是很像装饰,但是两者的着重点不同,装饰模式旨在扩展功能,这里是以代理Car类去解决问题,语义,出发点是不同的,前面讲到过设计模式的体会,这里还需要读者慢慢去体会。
这里跟着上面的思路来,我们引入一个新的问题:
这个问题暂且搁置,先看下一个问题,这个时候请注意,请将重点放到TimeCarProxy 上来。
首先我们需要一个代理对象:
用字符串表示代码后我们就可以任意的构造出我们想要的代码,让后将字符串输出到一个java文件中交给程序去编译
那程序要怎么编译java文件呢?
JDK6为我们提供了Complier API ,另外还有CGlib、ASM插件可以直接生成二进制文件不用再编译了,Spring中也支持通过CGlib方式实现动态代理
现在暂时不管生成字符串的逻辑,我们先解决编译的问题
代码:
效果图:
从图中我们可以生成了TmpProxy 的java文件和class文件
接下来我们又要考虑下一个问题了。
好了,到此为止,我们达到了动态生成代理对象的目的了,但是我们会发现还是只能动态生成TimeProxy,但是你别忘了,TimeProxy是由字符串生成的,而我们动态修改字符串是不是容易多了。接下来我们要做的就是修改字符串。
然后我们需要得到接口里的方法对代理类中的方法进行重新编排
修改好后我们可以动态的生成任何对象的代理对象,只是生成的代理对象固定的只能统计运行时间业务,所以我们还需要一个处理业务的逻辑。
分析:该接口定义了目标类方法的实现规则,所以我们在实现该接口的时候需要告知它目标类。
实现代码例子:
注意点:Proxy中的invoke实际上是调用的是handler中的invoke
如此一来,就可以实现处理任何的业务逻辑了,同时简单的代理模式也实现了。
在第一遍运行时会出现ClassNoFound异常,那是因为编译的class文件并没有马上写到目录下,重新运行就可以出来结果了,至于如何改这个bug我还没研究出来。
Car:
Move:
Proxy:
ProxyHandler:
TimeProxy:
Test:
体会设计模式
可能接触过设计模式的人都会有一种疑惑:感觉很多设计模式的实现方式都非常的相似,就比如说代理模式和装饰模式。确实有些设计模式的实现方式是差不多的,但是他们是从不同的场景出发,解决不同的问题的,我们需要从思想的角度来体会设计模式。代理模式
由一个实际问题来表达代理模式的思想注:源代码在最后,过程中的代码不一定能运行,请参看源代码
提出问题
新建一个Car类,包含一个drive方法,现在要求是在不改变Car代码的前提下计算drive运行的时间Car代码:
public class Car { public void drive() throws InterruptedException { System.out.println("开车了..."); Thread.sleep(1000); } }
解决统计时间的问题
如果要统计时间,我们现在要将代码写成这样:public class Car { /** * 解决这个问题我们需要在drive运行开始和结束的位置加入系统当前时间 * 再得到两个时间差 * @throws InterruptedException */ @Test public void drive() throws InterruptedException { //获取开始时间点 long i = System.currentTimeMillis(); System.out.println("开车了..."); Thread.sleep(1000); //获取结束时间点 long j = System.currentTimeMillis(); System.out.println("车开了"+(j-i)+"millis"); } }
解决不能使用改动drive代码的问题
现在是不能改动drive的那要怎么把计算时间的代码插入进去呢?这个时候我们就需要用到代理模式了,Car的代码不能改但是它的代理类可以,而代理方式也有两种如下:
1. 通过继承Car来代理
public class ExtendsCar extends Car { //这样我们可以通过重写Car的drive方法来实现代理 @Override public void drive() throws InterruptedException { long i = System.currentTimeMillis(); super.drive(); long j = System.currentTimeMillis(); System.out.println("车开了:"+(i+j)+"millis"); } }
另外一种方式通过聚合的代理
聚合,一个类中包含另外一个类。
用聚合的方式代理时,我们需要代理类和被代理类有同样的行为,我们可以通过实现相同的接口来实现
接口代码:
public interface Move { public void drive(); }
让Car类implement接口,这里就不贴代码了
代理类实现接口:
public class TimeCarProxy implements Move { Car c ; public TimeCarProxy(Car c){ this.c=c; } @Override public void drive() { long i = System.currentTimeMillis(); System.out.println("开始时间:" + i); move.drive(); long j = System.currentTimeMillis(); System.out.println("结束时间:" + j); System.out.println("车开了:"+(j-i)+"millis"); } }
问题:两种实现方式那个比较好?
这里还是以一个问题来表达:如果我还要在前面drive前后加上启动和停止怎么办?
以继承的方式实现,我们的思路是让它的代理类再被代理,代码如下:public class CarAction extends ExtendsCar { @Override public void drive() { System.out.println("启动车!"); super.drive(); System.out.println("停止车!"); } }
这种方式的问题: 1. 继承被占用,不能再继承其它类 2. 主要的问题:如果我要将他们的顺序反过来,先启动停止在计算时间的话,那不就意味着我们要重新写它的代理类吗?此时继承的局限性就显现出来了
2. 以聚合的方式实现:我们可以再来一个代理实现Move接口,代码如下:
public class Action implements Move { ImplementMove move ; public Action(ImplementMove move){ this.move=move; } @Override public void drive() { System.out.println("车启动!"); move.drive(); System.out.println("车停止!"); } }
到这里可能会有疑问,这不是跟继承存在一样的问题吗?别急,请看下面一种实现:
public class ActionCarProxy implements Move { /** * 因为它们有一个共同的特点就是实现了Move,若我们把聚合对象换成 * Move接口不就想让谁代理就谁代理了吗? * 同样我们也要将代理时间的类改成Move */ Move move ; public ActionCarProxy (Move move){ this.move=move; } @Override public void drive() { System.out.println("车启动!"); move.drive(); System.out.println("车停止!"); } }
可能会有点绕,这里写个测试类来理一理思路,测试类代码:
public class TestCar { public static void main(String args[]){ Car car = new Car(); //若我们想先开始计算时间再启动停止 // TimeCarProxy t = new TimeCarProxy(car); // ActionCarProxy a = new ActionCarProxy(t); // a.drive(); //若我们想先启动停止再计算时间 ActionCarProxy a = new ActionCarProxy(car); TimeCarProxy t = new TimeCarProxy(a); t.drive(); } }
运行效果:
1. 先计算时间再启动
2. 先启动再计算时间
以上就实现了一个静态代理。从上面的效果可以看出实现了预期的效果,我们可以任意的指定谁先代理谁后代理,可以看作是横向扩展了。
到这里会有很多人有疑惑,这不是装饰设计模式吗?从实现语法的角度来讲确实是很像装饰,但是两者的着重点不同,装饰模式旨在扩展功能,这里是以代理Car类去解决问题,语义,出发点是不同的,前面讲到过设计模式的体会,这里还需要读者慢慢去体会。
动态代理
注意:这里开始模仿JDK实现Proxy这里跟着上面的思路来,我们引入一个新的问题:
如过Car里有多个方法要求计算运行时间怎么处理?
这样的话我们需要在TimeCarProxy 中的每个方法前后获取当前时间并计算,那这样的话我们会发现TimeCarProxy中出现了很多的重复代码,当然我们可以给重复的代码简单封装,当那也没从根本上解决问题。这个问题暂且搁置,先看下一个问题,这个时候请注意,请将重点放到TimeCarProxy 上来。
如果我们需要TimeCarProxy 不仅代理Car还能代理其它类对象
也就是一个万能的TimeProxy代理,可以代理任意对象执行计算方法运行时间,这个时候我们需要怎么办?首先我们需要一个代理对象:
//jdk中Proxy就是动态代理 public class Proxy { //产生并返回一个代理对象 public static Object newProxyInstance(){ //我们需要在这里动态的生成代理对象 return null; } }
那我们要怎么动态生成对象呢?
我们首先要得到要有生成对象的代码,但是代码不能交给程序处理,所以我们要将代码转化成程序能处理的形式,那就是字符串。用字符串表示代码后我们就可以任意的构造出我们想要的代码,让后将字符串输出到一个java文件中交给程序去编译
那程序要怎么编译java文件呢?
JDK6为我们提供了Complier API ,另外还有CGlib、ASM插件可以直接生成二进制文件不用再编译了,Spring中也支持通过CGlib方式实现动态代理
现在暂时不管生成字符串的逻辑,我们先解决编译的问题
代码:
public class FileTest { @Test public void test() throws IOException { //用来生成代理对象的代理类的字符串形式 String src="" + "package net.hncu.test;\n" + "public class TmpProxy implements Move {\n" + " Move move ;\n" + " public TmpProxy(Move move){\n" + " this.move=move;\n" + " }\n" + " @Override\n" + " public void drive() {\n" + " long i = System.currentTimeMillis();\n" + " System.out.println(\"开始时间:\" + i);\n" + " move.drive();\n" + " long j = System.currentTimeMillis();\n" + " System.out.println(\"结束时间:\" + j);\n" + " System.out.println(\"车开了:\"+(j-i)+\"millis\");\n" + " }\n" + "}"; //将字符串保存成一个java文件System.getProperty("user.dir")获得项目路径 String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java"; //新建文件 File file = new File(filename); //将字符串写到文件 FileWriter fileWriter =new FileWriter(file); fileWriter.write(src); fileWriter.close(); /** * 编译java文件这里对编译过程不做过多阐述,如果感兴趣可以去查看api */ //获取java编译器jdk6支持 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //获取一个file管理器(三个参数diagnosticListener监听编译过程的监听器 // locale国际化相关,charset指定字符集)所有参数为空时,指定默认配置 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null); //根据文件名字拿到java文件对象(可以填多个文件,获得多个对象)返回一个文件对象的迭代器 Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename); //建立一次编译任务(参数:out输出位置,fileManager文件管理器,diagnosticListener监听器, // options编译时的参数,暂不填,classes编译时所需要的class文件,compilationUnits需要编译的单元) JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units); //执行编译 task.call(); //关闭文件管理器 fileManager.close(); } }
效果图:
从图中我们可以生成了TmpProxy 的java文件和class文件
接下来我们又要考虑下一个问题了。
我们需要把我们生成的class文件加载到内存来生成一个代理对象
这里只贴部分代码://加载class 文件到内存, //直接从指定URL位置加载class文件到内存,其实我们也可以直接将class存到bin目录下,但是可能会造成冲突 // 首先我们需要一个URL数组指定加载class文件的路径, URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")}; //新建一个URL类加载器 URLClassLoader classLoader = new URLClassLoader(urls); //加载路径下的指定class文件 Class aClass = null; try { aClass = classLoader.loadClass("net.hncu.test.TmpProxy"); } catch (ClassNotFoundException e) { e.printStackTrace(); } //System.out.println("aClass: "+aClass+" from: "+"FileTest.test"); //利用反射操作class对象 //构造一个实例 Constructor constructor = aClass.getConstructor(inter); return constructor.newInstance(object);
好了,到此为止,我们达到了动态生成代理对象的目的了,但是我们会发现还是只能动态生成TimeProxy,但是你别忘了,TimeProxy是由字符串生成的,而我们动态修改字符串是不是容易多了。接下来我们要做的就是修改字符串。
问题来了,我们需要如何修改字符串让其动态生成我们需要的代码呢?
首先我们需要生成任意对象的代理类,我们需要告诉它我们要生成代理类的规范,即被代理类的接口"public class TmpProxy implements "+inter.getName()+" {\n" + " public TmpProxy("+handler.getClass().getName()+" tmp){\n" + " this.tmp=tmp;\n" + " }\n" +
然后我们需要得到接口里的方法对代理类中的方法进行重新编排
//根据接口的class文件动态实现多个方法的代理字符串拼接 String methodStr=""; Method[] methods = inter.getMethods(); for (Method method : methods) { methodStr+= " @Override\n" + //这里方法名要改成当前的方法名 " public void "+method.getName()+"() {\n" + " try{\n"+ " Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+ " tmp.invoke(this,method);\n" + " }catch(Exception e){\n"+ " e.printStackTrace();\n"+ " }\n"+ " }\n";
修改好后我们可以动态的生成任何对象的代理对象,只是生成的代理对象固定的只能统计运行时间业务,所以我们还需要一个处理业务的逻辑。
那么我们需要怎样来修改业务逻辑呢?
因为业务逻辑是需要用户自己来定义的,所以不能写死在字符串中,当是业务逻辑需要有一定的编写规范,所以最好的选择就是通过一个接口来规范业务逻辑处理,让后让用户来实现接口定义自己的业务逻辑。public interface ProxyHandler { //在自定义方法模块时我们肯定要执行被代理类本身的方法, //所以我们至少需要以下两个参数 public void invoke(Object object,Method m); }
分析:该接口定义了目标类方法的实现规则,所以我们在实现该接口的时候需要告知它目标类。
实现代码例子:
public class TimeHandler implements ProxyHandler { Object target; public TimeHandler(Object target){ this.target=target; } @Override public void invoke(Object object,Method m) { long start = System.currentTimeMillis(); System.out.println("开始时间:"+start); try { m.invoke(target); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } long end= System.currentTimeMillis(); System.out.println("结束时间:"+end); System.out.println("用时:"+(end-start)); } }
注意点:Proxy中的invoke实际上是调用的是handler中的invoke
如此一来,就可以实现处理任何的业务逻辑了,同时简单的代理模式也实现了。
结论
目前这个只是简单的实现,只能做没有参数列表、返回值的,后续有时间再去完善源码
注意:在第一遍运行时会出现ClassNoFound异常,那是因为编译的class文件并没有马上写到目录下,重新运行就可以出来结果了,至于如何改这个bug我还没研究出来。
Car:
package net.hncu.test; import org.junit.Test; /** * Project: String1 * Desc: proxy test * Author: AMX50B * Date: 2017-10-20 19:08 */ public class Car implements Move { /** * 解决这个问题我们需要在drive运行开始和结束的位置加入系统当前时间 * 再得到两个时间差 */ @Test public void drive(){ System.out.println("开车了..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void flay() { System.out.println("起飞了..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
Move:
package net.hncu.test; /** * Created by AMX50B on 2017/10/20 */ public interface Move { public void drive(); public void flay(); }
Proxy:
package net.hncu.test; import org.junit.Test; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; /** * Project: String1 * Desc: proxy * Author: AMX50B * Date: 2017-10-21 12:51 */ public class Proxy { //产生并返回一个代理对象 @Test public static Object newProxyInstance(Class inter,ProxyHandler handler) throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //用来生成代理对象的代理类的字符串形式 //根据接口的class文件动态实现多个方法的代理字符串拼接 String methodStr=""; Method[] methods = inter.getMethods(); for (Method method : methods) { methodStr+= " @Override\n" + //这里方法名要改成当前的方法名 " public void "+method.getName()+"() {\n" + " try{\n"+ " Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+ " tmp.invoke(this,method);\n" + " }catch(Exception e){\n"+ " e.printStackTrace();\n"+ " }\n"+ " }\n"; } String src="" + "package net.hncu.test;\n" + "import java.lang.reflect.Method;\n"+ //传入接口名称,使代理类能动态代理任意我们指定的接口 "public class TmpProxy implements "+inter.getName()+" {\n" + " "+handler.getClass().getName()+" tmp ;\n" + " public TmpProxy("+handler.getClass().getName()+" tmp){\n" + " this.tmp=tmp;\n" + " }\n" + methodStr + "}\n"; //将字符串保存成一个java文件System.getProperty("user.dir")获得项目路径 String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java"; //新建文件 File file = new File(filename); //将字符串写到文件 FileWriter fileWriter =new FileWriter(file); fileWriter.write(src); fileWriter.close(); /** * 编译java文件这里对编译过程不做过多阐述,如果感兴趣可以去查看api */ //获取java编译器jdk6支持 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //获取一个file管理器(三个参数diagnosticListener监听编译过程的监听器 // locale国际化相关,charset指定字符集)所有参数为空时,指定默认配置 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null); //根据文件名字拿到java文件对象(可以填多个文件,获得多个对象)返回一个文件对象的迭代器 Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename); //建立一次编译任务(参数:out输出位置,fileManager文件管理器,diagnosticListener监听器, // options编译时的参数,暂不填,classes编译时所需要的class文件,compilationUnits需要编译的单元) JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units); //执行编译 task.call(); //关闭文件管理器 fileManager.close(); //加载class 文件到内存, //直接从指定URL位置加载class文件到内存,其实我们也可以直接将class存到bin目录下,但是可能会造成冲突 // 首先我们需要一个URL数组指定加载class文件的路径, URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")}; //新建一个URL类加载器 URLClassLoader classLoader = new URLClassLoader(urls); //加载路径下的指定class文件 Class aClass = null; try { aClass = classLoader.loadClass("net.hncu.test.TmpProxy"); } catch (ClassNotFoundException e) { try { Thread.sleep(1000); aClass= classLoader.loadClass("net.hncu.test.TmpProxy"); } catch (InterruptedException e1) { e1.printStackTrace(); } catch (ClassNotFoundException e1) { e1.printStackTrace(); } } //System.out.println("aClass: "+aClass+" from: "+"FileTest.test"); //利用反射操作class对象 //构造一个实例 Constructor constructor = aClass.getConstructor(handler.getClass()); return constructor.newInstance(handler); // move.drive(); } }
ProxyHandler:
package net.hncu.test;
import java.lang.reflect.Method;
/**
* Created by AMX50B on 2017/10/23
*/
public interface ProxyHandler { //在自定义方法模块时我们肯定要执行被代理类本身的方法, //所以我们至少需要以下两个参数 public void invoke(Object object,Method m); }
TimeProxy:
package net.hncu.test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Project: String1
* Desc: time handler
* Author: AMX50B
* Date: 2017-10-23 18:40
*/
public class TimeHandler implements ProxyHandler { Object target; public TimeHandler(Object target){ this.target=target; } @Override public void invoke(Object object,Method m) { long start = System.currentTimeMillis(); System.out.println("开始时间:"+start); try { m.invoke(target); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } long end= System.currentTimeMillis(); System.out.println("结束时间:"+end); System.out.println("用时:"+(end-start)); } }
Test:
package net.hncu.test; /** * Project: String1 * Desc: test car * Author: AMX50B * Date: 2017-10-21 9:58 */ public class TestCar { public static void main(String args[]){ Car car = new Car(); //若我们想先开始计算时间再启动停止 // TimeCarProxy t = new TimeCarProxy(car); // ActionCarProxy a = new ActionCarProxy(t); // a.drive(); //若我们想先启动停止再计算时间 // ActionCarProxy a = new ActionCarProxy(car); // TimeCarProxy t = new TimeCarProxy(a); // t.drive(); try { ProxyHandler proxyHandler = new TimeHandler(car); Move m = (Move) Proxy.newProxyInstance(Move.class,proxyHandler); m.drive(); m.flay(); } catch (Exception e) { e.printStackTrace(); } } }
相关文章推荐
- 代理模式及JDK动态代理(InvocationHandler)的简单实现与分析
- JDK动态代理实现简单AOP--转
- JDK和CGLib两种方式实现动态代理模式
- 反射实现 AOP 动态代理模式(Spring AOP 的实现 原理)
- 动态代理模式(抽象角色用接口形式,代理角色必须实现InvocationHandler)
- iOS的动态代理模式的实现
- 反射实现 AOP 动态代理模式(Spring AOP 的实现 原理)
- 动态代理--简单实现
- Java基础之反射及动态代理,反射实现工厂模式
- 1007--反射实现 AOP 动态代理模式(Spring AOP 的实现 原理)
- 反射实现 AOP 动态代理模式(Spring AOP 的实现 原理)
- (Dynamic Proxy)动态代理模式的Java实现
- 通过动态代理(Proxy)实现的数据库连接池的创建连接与归还链接的操作的简单的实现流程
- 代理模式之静态代理的简单实现---理解AOP(面向切面编程)的前奏---想要理解AOP,必须先理解一下
- SSM框架day01——025——CGLIB动态代理模式、026——CGLIB动态代理模式的实现、027——CGLIB动态代理执行流程分析、CGLIB动态代理的MethodProxy参数
- Java 动态代理的实现-代理模式--aop
- JDK中的Poxy类简单实现动态代理
- Cglib动态代理模式实现
- JDK动态代理实现简单AOP--转