Java设计模式——动态代理
java提供了动态代理的对象,本文主要探究它的实现,
动态代理是AOP(面向切面编程, Aspect Oriented Programming)的基础实现方式,
动态代理使代码的重复更少,更便与维护
本文参考了满一行老师和马士兵老师的视频,在此表示Thanks。
假设小张通过QQ和一个网名为如花的妹纸聊天,
而在QQ的另一端,这个网名为如花的妹纸的name是小丽
但是,正在聊天的时候小丽生病了不想打字,小丽就找她的男朋友帮忙回应
小丽的男朋友在如花的QQ账号上与小张聊天,小张并不知道和他聊天的如花是谁
小张发来消息了,"HI, 你好,",小丽的男朋友说,“小张发来消息了‘HI, 你好’,我该怎么回?”
小丽说,那就回一个“你也好, 哈哈”吧, 小张的男朋友按照小丽说的做了。
这就是代理模式的雏形,小丽实现了如花这个接口,小张也实现了如花这个接口
小张调用的是如花这个接口,但是他并不知道,和他聊天的是小丽,还是小丽的男朋友
小丽的男朋友因为在信息的必经道上,所以可以对信息进行扭曲以及篡改,对不良的信息过滤
哈哈,这就是面向切面编程的一个例子了。
故事就讲到这里,下面来解释一下静态代理
Tank类实现一个Movable接口,怎样知道它的运行时间呢方法move()的运行时间呢
我们可以在方法前面和后面都记录时间, long start = System.currentTimeMillis();
/Proxy/src/yuki/design/proxy/package1/Movable.java
package yuki.design.proxy.package1; public interface Movable { void move(); }
/Proxy/src/yuki/design/proxy/package1/Tank.java
package yuki.design.proxy.package1; import java.util.Random; public class Tank implements Movable { @Override public void move() { // long start = System.currentTimeMillis(); System.out.println("Tank is moving..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } // long end = System.currentTimeMillis(); // System.out.println("time:" + (start-end)); } }
通过注释掉的内容可以计算出夹在中间代码的运行时间
这种方法需要修改源码,但是很多方法都是不知道源码的class文件
这个方式拿到的也不是方法本身运行的时间,因为方法已经被篡改了
还有一种方法是要在main方法中把方法执行的前后加上时间的记录
这是不行的,因为jdk为方法的运行准备也需要时间,这显然是不精确的
哦,我们可以继承Tank这个类,重写它的方法就可以了
用继承把原来的方法前后加一些逻辑,原来的方法就用super调用
/Proxy/src/yuki/design/proxy/package1/Tank2.java
package yuki.design.proxy.package1; public class Tank2 extends Tank { @Override public void move() { long start = System.currentTimeMillis(); super.move(); long end = System.currentTimeMillis(); System.out.println("time:" + (start-end)); } }
一个类里面有另一个类的对象,这种对象间的关系叫做聚合,
它也可以实现上面的继承所提到的效果,只是把父类改成代理类的一个成员变量而已
Tank3是用聚合的方式实现的
/Proxy/src/yuki/design/proxy/package1/Tank3.java
package yuki.design.proxy.package1; public class Tank3 implements Movable { public Tank3(Tank t) { this.t = t; } Tank t; @Override public void move() { long start = System.currentTimeMillis(); t.move(); long end = System.currentTimeMillis(); System.out.println("time:" + (start-end)); } }
继承明显是不灵活的,也不符合日常的逻辑
一般来说,如果可能这个类有多个代理类呢,比如说,添加一个记录日志的类
功能上的叠加,先记录日志,后记录时间;或者是相反的顺序
如果用继承的方式,会导致类越来越多
如果用聚合的方式,可以代理Movable接口,这里的关键是实现同一接口
/Proxy/src/yuki/design/proxy/package1/TankTimeProxy.java
package yuki.design.proxy.package1; public class TankTimeProxy implements Movable { public TankTimeProxy(/*Tank*/Movable t) { this.t = t; } /*Tank*/Movable t; @Override public void move() { long start = System.currentTimeMillis(); t.move(); long end = System.currentTimeMillis(); System.out.println("time:" + (end-start)); } }
/Proxy/src/yuki/design/proxy/package1/TankLogProxy.java
package yuki.design.proxy.package1; public class TankLogProxy implements Movable{ public TankLogProxy(/*Tank*/ Movable t) { this.t = t; } /*Tank*/Movable t; @Override public void move() { System.out.println("Tank start......"); t.move(); System.out.println("Tank stop......"); } }
注意,这里代理类的对象并不是真实的对象,而是接口,通过这个接口
这两个代理类和被代理的对象就可以自由组合
严格的说,在被代理对象和最终的代理对象之间可以随机插入代理类
每一个代理类就可以看作是一个切片
/Proxy/src/yuki/design/proxy/package1/Client.java
package yuki.design.proxy.package1; public class Client { public static void main(String[] args) { Tank t = new Tank(); TankTimeProxy timeProxy = new TankTimeProxy(t); TankLogProxy logProxy = new TankLogProxy(timeProxy); Movable m = logProxy; m.move(); /* Movable logProxy = new TankLogProxy(t); Movable timeLogProxy = new TankTimeProxy(logProxy); timeLogProxy.move(); */ } }
spring这个轻量级容器,提供了继承和聚合实现代理的方式,
但是,强烈建议我们使用聚合的哪一种
可以说,AOP是动态代理的一个应用
还要说明的是,当一个接口有多个方法时,相同的代码必须重复
就是封装成对象也至少要插入一条插入调用方法的语句
/Proxy/src/yuki/design/proxy/package2/Movable.java
package yuki.design.proxy.package2; public interface Movable { void move(); void stop(); }
/Proxy/src/yuki/design/proxy/package2/Tank.java
package yuki.design.proxy.package2; import java.util.Random; public class Tank implements Movable { @Override public void move() { System.out.println("Tank is moving..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void stop() { System.out.println("Tank is stopping..."); } }
/Proxy/src/yuki/design/proxy/package2/TankTimeProxy.java
package yuki.design.proxy.package2; public class TankTimeProxy implements Movable { public TankTimeProxy(Movable t) { this.t = t; } Movable t; @Override public void move() { long start = System.currentTimeMillis(); t.move(); long end = System.currentTimeMillis(); System.out.println("time:" + (end-start)); } @Override public void stop() { long start = System.currentTimeMillis(); t.stop(); long end = System.currentTimeMillis(); System.out.println("time:" + (end-start)); } }
如果Movable接口里还有其它的方法,计算时间的代码在代理类中还得重写
可以把相同的代码放在方法中,before,after
怎样让计算时间的代理变得更加通用,
不只是计算坦克的时间,还可以计算汽车的
怎样写一个通用的时间代理
我们应该怎样生成一个代理呢
动态代理的意思是,不再看到代理类的名字,需要的是代理对象直接产生
加入能把源码编译完产生新的类,再把这个类放入内存,产生新的对象
怎样对这段代码动态的编译,动态编译完成就能产生动态代理了
现在看不到这个动态代理类的名字
这里可以自定义proxy的实现,而具体的类就不用写了
/Proxy/src/yuki/design/proxy/package3/Movable.java
/Proxy/src/yuki/design/proxy/package3/Tank.java
package yuki.design.proxy.package3; import java.util.Random; public class Tank implements Movable { @Override public void move() { // long start = System.currentTimeMillis(); System.out.println("Tank is moving..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } // long end = System.currentTimeMillis(); // System.out.println("time:" + (start-end)); } }
package yuki.design.proxy.package3; import java.util.Random; public class Tank implements Movable { @Override public void move() { // long start = System.currentTimeMillis(); System.out.println("Tank is moving..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } // long end = System.currentTimeMillis(); // System.out.println("time:" + (start-end)); } }
在JDK6里,提供了编译java代码的API
获取项目的根路径System.getProperty("user.dir");
通过FileWriter的write方法把字符串写到文件中
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler是空值,把/Java/Installed JREs由jre改为jdk
这里的jre还是从jdk下的jre下拿的
/JavaBuildPath/Libraries选择Workspace default JRE(jdk1.8.0)
在Eclipse中,必须先编译好,有了文件后才能加载类
使用反射加载对象,运行方法
/Proxy/src/yuki/design/proxy/Proxy.java
package yuki.design.proxy; import java.io.File; import java.io.FileWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; public class Proxy { public static Object newProxyInstance(Class<?> interfaces, InvocationHandler h) throws Exception { String rt = "\r\n"; String methodStr = ""; Method[] methods = interfaces.getMethods(); /*for(Method m : methods){ methodStr += " @Override" + rt + " public void "+ m.getName() +"(){" + rt + " long start = System.currentTimeMillis();"+rt+ " System.out.println(\"starttime:\" + start);"+rt+ " t."+ m.getName() +"();"+rt+ " long end = System.currentTimeMillis();"+rt+ " System.out.println(\"time:\" + (end-start));"+rt+ " }"; }*/ for(Method m : methods){ methodStr += " @Override" + rt + " public void "+ m.getName() +"() {" + rt + " try{" + rt + " Method md = "+ interfaces.getName() +".class.getMethod(\""+ m.getName() +"\");" + rt + " h.invoke(this, md);"+rt+ " }catch(Exception e){ e.printStackTrace(); }" + rt + " }"; } /*String src = "package yuki.design.proxy.package3;"+rt+ "public class TankTimeProxy implements "+ interfaces.getName() +" {"+rt+ " public TankTimeProxy("+ interfaces.getName() +" t) {"+rt+ " this.t = t;"+rt+ " }"+rt+ " "+ interfaces.getName() +" t;"+rt+ methodStr +rt+ "}";*/ String src = "package yuki.design.proxy.package3;"+rt+ "import yuki.design.proxy.InvocationHandler;"+rt+ "import java.lang.reflect.Method;"+rt+ "public class TankTimeProxy implements "+ interfaces.getName() +" {"+rt+ " public TankTimeProxy(InvocationHandler h) {"+rt+ " this.h = h;"+rt+ " }"+rt+ " InvocationHandler h;"+rt+ methodStr +rt+ "}"; String fileName = System.getProperty("user.dir") + "/temp/yuki/design/proxy/package3/TankTimeProxy.java"; File f = new File(fileName); if(!f.exists()){ f.mkdirs(); f.delete();} FileWriter fw = new FileWriter(f); fw.write(src); fw.flush(); fw.close(); /* * compile */ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // System.out.println(compiler); //com.sun.tools.javac.api.JavacTool@a570f StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(fileName); CompilationTask task = compiler.getTask(null, fileManager, null, null, null, units); task.call(); fileManager.close(); /* * load into memory and create an instance */ URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/temp/")}; //System.out.println(urls[0]); //file:/D:/Workspaces/Eclipse/Proxy/bin URLClassLoader ul = new URLClassLoader(urls); Class<?> c = ul.loadClass("yuki.design.proxy.package3.TankTimeProxy"); //System.out.println(c); //class yuki.design.proxy.package3.TankTimeProxy ul.close(); Constructor<?> constructor = c.getConstructor(InvocationHandler.class); Object m = constructor.newInstance(h); return m; } }
现在的代理只能实现Movable接口,现在要求可以实现任意接口的
传入Class<?>类型的参数就可以了
站在ClassLoader的角度Method也是一系列的对象
假设有很多方法就遍历所有的方法
往里传入任意接口,就可以产生实现了这个接口的任意对象
更换为其它的目录后,一次运行就可以成功
原先在src目录下,会有两份class文件,
一份是eclipse编译的,一份是ClassLoader加载的
/Proxy/src/yuki/design/proxy/InvocationHandler.java
package yuki.design.proxy; import java.lang.reflect.Method; public interface InvocationHandler { void invoke(Object o, Method m); }
这里,实现了InvocationHandler的invoke(Obejct, Method)方法
/Proxy/src/yuki/design/proxy/TimeHandler.java
package yuki.design.proxy; import java.lang.reflect.Method; public class TimeHandler implements InvocationHandler { private Object target; public TimeHandler(Object target) { this.target = target; } @Override public void invoke(Object o, Method m) { long start = System.currentTimeMillis(); System.out.println("starttime:" + start); try { m.invoke(target); } catch (Exception e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("time:" + (end-start)); } }
我们来看一看它是如何被调用的吧
/Proxy/src/yuki/design/proxy/Client.java
package yuki.design.proxy; import yuki.design.proxy.package3.Movable; import yuki.design.proxy.package3.Tank; public class Client { public static void main(String[] args) throws Exception { Tank t = new Tank(); InvocationHandler h = new TimeHandler(t); Movable m = (Movable) Proxy.newProxyInstance(Movable.class, h); // Movable m = (Movable) Proxy.newProxyInstance(Comparable.class); m.move(); } }
看一看生成的代理java文件和class文件
打开TankTimeProxy.java
package yuki.design.proxy.package3; import yuki.design.proxy.InvocationHandler; import java.lang.reflect.Method; public class TankTimeProxy implements yuki.design.proxy.package3.Movable { public TankTimeProxy(InvocationHandler h) { this.h = h; } InvocationHandler h; @Override public void move() { try{ Method md = yuki.design.proxy.package3.Movable.class.getMethod("move"); h.invoke(this, md); }catch(Exception e){ e.printStackTrace(); } } }
它被翻译成了class文件,加载进内存,生成对象
if(!f.exists()){ f.mkdirs(); f.delete();}
这一句的意思是,如果文件不存在就建立这些文件,但是在文件的树梢
f.mkdirs()之后,有一个TimeTankProxy.java的文件夹,
导致在该目录下不能新建TimeTankProxy.java的文件,所以要f.delete();
运行看一下结果吧,控制台打印输出如下语句
starttime:1407701374031 Tank is moving... time:5934
补充一些小细节,
/Proxy/src/yuki/design/proxy/complier/Test1.java
package yuki.design.proxy.complier; import java.io.File; import java.io.FileWriter; import java.lang.reflect.Constructor; import java.net.URL; import java.net.URLClassLoader; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import yuki.design.proxy.package3.Movable; import yuki.design.proxy.package3.Tank; public class Test1 { public static void main(String[] args) throws Exception { String rt = "\r\n"; String src = "package yuki.design.proxy.package3;"+rt+ "public class TankTimeProxy implements Movable {"+rt+ " public TankTimeProxy(Movable t) {"+rt+ " this.t = t;"+rt+ " }"+rt+ " Movable t;"+rt+ " @Override"+rt+ " public void move() {"+rt+ " long start = System.currentTimeMillis();"+rt+ " System.out.println(\"starttime:\" + start);"+rt+ " t.move();"+rt+ " long end = System.currentTimeMillis();"+rt+ " System.out.println(\"time:\" + (end-start));"+rt+ " }"+rt+ "}"; String fileName = System.getProperty("user.dir") + "/src/yuki/design/proxy/package3/TankTimeProxy.java"; File f = new File(fileName); FileWriter fw = new FileWriter(f); fw.write(src); fw.flush(); fw.close(); /* * compile */ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); // System.out.println(compiler); //com.sun.tools.javac.api.JavacTool@a570f StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(fileName); CompilationTask task = compiler.getTask(null, fileManager, null, null, null, units); task.call(); fileManager.close(); /* * load into memory and create an instance */ URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")}; //System.out.println(urls[0]); //file:/D:/Workspaces/Eclipse/Proxy/bin URLClassLoader ul = new URLClassLoader(urls); Class<?> c = ul.loadClass("yuki.design.proxy.package3.TankTimeProxy"); //System.out.println(c); //class yuki.design.proxy.package3.TankTimeProxy ul.close(); Constructor<?> constructor = c.getConstructor(Movable.class); Movable m = (Movable) constructor.newInstance(new Tank()); m.move(); } }
/Proxy/src/yuki/design/proxy/complier/Test2.java
package yuki.design.proxy.complier; import java.lang.reflect.Method; import yuki.design.proxy.package3.Movable; public class Test2 { public static void main(String[] args) { Method[] methods = Movable.class.getMethods(); for(Method m : methods){ System.out.println(m.getName()); } } }
现在,我们一起来连贯一下代理的主要细节:
一个代理类TimeHandler实现一个接口InvocationHandler
接口的方法是invoke(Object, Method)
这个类需要传入一个被代理对象Tank,
并提供一个它的每个方法需要添加的代码,在invoke中调用Tank的方法
它会被Proxy的生成器吸收,遍历这个类的所有方法
最后会在一个临时的目录生成代码并编译,在生成的类中,
它吸收了InvocationHandler,
并通过invoke的方法参数调用被代理类的每个方法
生成Class文件,加载进内存,获得了这个代理类的对象
这时,调用这个代理类的每个方法都会执行在TimeHandler中被填入的内容
使用代理类实现不修改原来的代码,在代码前后添加逻辑
面向切面编程,这个逻辑是可插拔的,可以把逻辑卸载配置文件中
配置是可以叠加的,
既然动态代理的类已写好,我们让它来实现一些其它的代理逻辑
/Proxy/src/yuki/design/proxy/test/UserMgr.java
package yuki.design.proxy.test; public interface UserMgr { void addUser(); }
/Proxy/src/yuki/design/proxy/test/UserMgrImpl.java
package yuki.design.proxy.test; public class UserMgrImpl implements UserMgr { @Override public void addUser() { System.out.println("1.插入记录到user表"); System.out.println("2.记录日志到日志表"); } }
/Proxy/src/yuki/design/proxy/test/TransactionHandler.java
package yuki.design.proxy.test; import java.lang.reflect.Method; import yuki.design.proxy.InvocationHandler; public class TransactionHandler implements InvocationHandler { private Object target; public TransactionHandler(Object target) { this.target = target; } @Override public void invoke(Object o, Method m) { System.out.println("Transaction start"); try { m.invoke(target); } catch (Exception e) { e.printStackTrace(); } System.out.println("Transaction commit"); } }
/Proxy/src/yuki/design/proxy/test/Client.java
package yuki.design.proxy.test; import java.lang.reflect.Method; import yuki.design.proxy.InvocationHandler; public class TransactionHandler implements InvocationHandler { private Object target; public TransactionHandler(Object target) { this.target = target; } @Override public void invoke(Object o, Method m) { System.out.println("Transaction start"); try { m.invoke(target); } catch (Exception e) { e.printStackTrace(); } System.out.println("Transaction commit"); } }
运行一下,就会看到结果,编译的文件以及控制台输出的结果
package yuki.design.proxy.package3; import yuki.design.proxy.InvocationHandler; import java.lang.reflect.Method; public class TankTimeProxy implements yuki.design.proxy.test.UserMgr { public TankTimeProxy(InvocationHandler h) { this.h = h; } InvocationHandler h; @Override public void addUser() { try{ Method md = yuki.design.proxy.test.UserMgr.class.getMethod("addUser"); h.invoke(this, md); }catch(Exception e){ e.printStackTrace(); } } }
starttime:1407702024556 Transaction start 1.插入记录到user表 2.记录日志到日志表 Transaction commit time:0
在最后用到了两个代理类的嵌套,它也可以用配置文件变成灵活的,即配置是可叠加的
jdk的动态代理有Proxy类的
newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)
接口InvocationHandler有方法
invoke(Object, Method, Object[])
简单的小思考:
什么叫动态代理?
动态代理是怎么产生的?
动态代理有什么用?
怎样使用JavaAPI生成动态代理呢
/Proxy/src/yuki/design/proxy/java/EmpService.java
package yuki.design.proxy.java; public interface EmpService { void save(); void update(); void delete(int id); }
/Proxy/src/yuki/design/proxy/java/EmpServiceImpl.java
package yuki.design.proxy.java; public class EmpServiceImpl implements EmpService { public void save() { System.out.println("EmpService save"); } @Override public void update() { System.out.println("EmpService update"); } @Override public void delete(int id) { System.out.println("EmpService delete, id="+ id); } }
/Proxy/src/yuki/design/proxy/java/EmpServiceProxy.java
package yuki.design.proxy.java; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class EmpServiceProxy { public static void main(String[] args) { //被代理的目标对象 final EmpService target = new EmpServiceImpl(); /** * 用来产生代理的类 * 第一个参数:classLoader, 用来加载*.class文件,类加载器 * 需要拿到当前线程的类加载器 * 第二个参数:代理类应当实现的接口,可以实现多个接口 * 第三个参数:一个接口 */ ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class<?>[] interfaces = new Class[]{EmpService.class}; InvocationHandler h = new InvocationHandler() { /** * Object proxy 代理对象自己 * Method method 代理对象正在被调用的那个方法,是在接口里定义的 * Object[] args 方法的参数数组 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法名:" + method.getName()); try { System.out.println("开始事物"); // 调用真正业务逻辑 System.out.println("==============="); method.invoke(target, args); System.out.println("==============="); System.out.println("提交事务"); } catch (Exception e) { System.out.println("回滚事务"); } finally { System.out.println("释放资源"); } return null; } }; EmpService service = (EmpService) Proxy.newProxyInstance(loader, interfaces, h); // service.save(); // service.update(); service.delete(111); } }
运行,会在控制台打印出如下语句
方法名:delete 开始事物 =============== EmpService delete, id=111 =============== 提交事务 释放资源
整个演示项目的包路径如下:
对于面向切面变成来说,有切入点和切面两个术语
于切入点匹配的类才会由spring生成动态代理,在spring中,我们需要写切入点表达式
切面是对应的附加操作,可以把这部分操作写在通知类中
<bean class="通知类"></bean>
<aop:config>
<aop:pointcut id="切入点id" expression="切入点表达式"></aop:pointcut>
<aop:aspect ref="通知类id">
<aop:通知类型 method="通知类中的方法" pointcut-ref="切入点id"></<aop:通知类型>
</aop:aspect>
</aop:config>
Object 代理对象 = context.getBean("目标对象id");
将主业务逻辑的附加操作抽取出去,根据功能不同,抽取为不同通知类的思想,称为面向切面编程
更多好文请查看:http://www.cnblogs.com/kodoyang/
孔东阳
2014/8/11