详述代理模式及动态代理简单实现
来源:互联网 发布:男士休闲短靴 知乎 编辑:程序博客网 时间:2024/05/21 09:40
前言:本文章总结于马士兵老师系列教程,是根据视频中提出的问题的思维为大纲写的
体会设计模式
可能接触过设计模式的人都会有一种疑惑:感觉很多设计模式的实现方式都非常的相似,就比如说代理模式和装饰模式。确实有些设计模式的实现方式是差不多的,但是他们是从不同的场景出发,解决不同的问题的,我们需要从思想的角度来体会设计模式。
代理模式
由一个实际问题来表达代理模式的思想
注:源代码在最后,过程中的代码不一定能运行,请参看源代码
提出问题
新建一个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(); } }}
- 详述代理模式及动态代理简单实现
- 代理模式及Java实现动态代理
- 代理模式及Java实现动态代理
- 代理模式及动态代理
- 代理模式(普通代理及动态代理)
- 静态代理及动态代理原理及简单实现
- 代理模式及JDK动态代理(InvocationHandler)的简单实现与分析
- 代理模式-Java动态代理的实现及应用
- 动态代理--简单实现
- 动态代理--简单实现
- 动态代理简单实现
- 代理模式之简单的动态代理
- java代理模式及动态代理类
- 代理及动态代理模式(Proxy、DynamicProxy)
- 代理模式及Java动态代理
- Java代理模式及动态代理
- Java代理模式及动态代理
- Java 中的代理模式及动态代理
- Hello, World
- 【BZOJ1477】【扩展欧几里得】青蛙的约会 题解
- 深度学习的几个重要关键点解释
- 'ascii' codec can't decode byte 0xe7 in position 12: ordinal not in range(128)的错误
- shiro源码分析篇1:前言
- 详述代理模式及动态代理简单实现
- 再谈STM32的CAN过滤器-bxCAN的过滤器的4种工作模式以及使用方法总结
- linux上安装mysql并配置远程访问
- 【实验报告】高软实验5
- Hyperledger Fabric 排序节点启动过程
- 51nod 1681公共祖先
- hadoop入门十四(pig)
- angular和路由实现页面跳转/姓名筛选/年龄区间查找/单/批量删除
- cup