动态代理
1.class Tank实现IMoveable接口,实现了move()方法
package com.sj.tank; public interface IMoveable { void move(); }
package com.sj.tank; import java.util.Random; public class Tank implements IMoveable { public void move() { System.out.println("Tank Moving..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } }
2. 假如不知道Tank类的源码,不能在其中的move()方法中直接添加代码的情况下,怎么知道move方法的执行时间?
1)TankInherit继承Tank,重写其中的move()方法,在调用super.move()方法的前后添加逻辑,来记录时间:
public class TankInherit extends Tank { @Override public void move() { long start = System.currentTimeMillis(); super.move(); long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } }
2)聚合,实现接口的方式,TankImplementingInterface也实现IMoveable接口,保存Tank类,在调用Tank.move方法的前后添加逻辑,来记录时间:
public class TankImplementingInterface implements IMoveable { private Tank tank; public TankImplementingInterface(Tank tank) { this.tank = tank; } public void move() { long start = System.currentTimeMillis(); System.out.println("starttime: " + start); tank.move(); long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } }
继承、实现接口(聚合)两种方式都实现了对class Tank中move()方法的代理,哪种方法好呢?——聚合的方式好些,继承的方式不灵活
为什么?
比如还有个记录Tank move方法的日志代理:TankLogProxy:
public class TankLogProxy implements IMoveable { private IMoveable iMoveable; public TankLogProxy(IMoveable iMoveable) { this.iMoveable = iMoveable; } public void move() { System.out.println("Tank Start..."); iMoveable.move(); System.out.println("Tank Stop."); } }
现在考虑功能的叠加:先记录日志,再记录时间:
如果用继承方式,TankInherit的代码就要改动了;或者弄个Tank4 extends TankInherit;要实现的代理功能更多,类就会无限制的增加下去;而且各个功能顺序可能不一样,比如先记录时间,再记录日志。
实现接口(聚合、实现同一接口)的方式更容易实现:
TankTimeProxy和TankLogProxy都实现了IMoveable接口,两个类可以互相代理:
public class TankTimeProxy implements IMoveable { private IMoveable iMoveable; public TankTimeProxy(IMoveable iMoveable) { this.iMoveable = iMoveable; } public void move() { long start = System.currentTimeMillis(); System.out.println("starttime: " + start); iMoveable.move(); long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } }
测试代码:
public class Client { public static void main(String[] args) { IMoveable tank = new Tank(); //日志代理,记录tank.move()执行时间 IMoveable timeProxy = new TankTimeProxy(tank); timeProxy.move(); System.out.println("---------------------------------"); //在时间外面加一层日志 IMoveable logProxyTimeProxy = new TankLogProxy(timeProxy); logProxyTimeProxy.move(); } }
Tank、TankTimeProxy、TankLogProxy都实现了IMoveable接口,TankTimeProxy和TankLogProxy可以互相代理。
timeProxy 和 logProxyTimeProxy 都是 IMoveable 类型的对象,logProxyTimeProxy 是 timeProxy 的代理对象。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
以上的部分是静态代理,不够强大。
比如 IMoveable中还有个方法:void stop();class Tank势必会实现 stop()方法;TankTimeProxy也要记录stop()方法的执行时间,
那么记录时间的代码:
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
也要在stop方法中重写一遍。
就像这样:
public interface Moveable { void move(); void stop(); }
public class Tank implements Moveable { public void move() { System.out.println("Tank Moving..."); try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } public void stop() { System.out.println("Tank Stopping..."); } }
public class TankTimeProxy implements Moveable { private Moveable moveable; public TankTimeProxy(Moveable moveable) { this.moveable = moveable; } public void move() { long start = System.currentTimeMillis(); System.out.println("starttime: " + start); moveable.move(); long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } public void stop() { long start = System.currentTimeMillis(); System.out.println("starttime: " + start); moveable.stop(); long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } }
会发现TankTimeProxy中move()和stop()中两段代码几乎一样,于是将它们封装起来:
比如beforeMethod(), afterMethods(), 貌似这些代码可以重用了。
现在考虑这个问题:
TankTimeProxy不叫TankTimeProxy了,而是叫TimeProxy,可以把任何一个对象当做被代理对象,可以计算这个对象里面的任意一个方法的运行时间,怎么做?
原始的这种方法肯定不行了,因为假如一个系统里面有100个类,要知道这100个类里面的方法运行了多重时间,那么就要为这100个类写代理对象,显然太不行了。
思路:
写个通用的时间代理类,可以对任意的对象进行代理,对象里面的任意方法都可以重写,前面加上start,后面加上end来计算时间:
1)我们假设被代理的对象都实现了某个接口,真正代理的时候是根据这个接口来生成代理对象的,
怎么样生成这个代理对象呢?
2)假设com.sj.proxy.Proxy1能产生一个代理类——Proxy1.java:
public class Proxy1 { //用来产生新的代理类 public static Object newProxyInstance() { String rt = "\r\n"; String src = "package com.sj.proxy;" + rt + "public class TankTimeProxy implements Moveable {" + rt + " private Moveable moveable;" + rt + " public TankTimeProxy(Moveable moveable) {" + rt + " this.moveable = moveable;" + rt + " }" + rt + " public void move() {" + rt + " long start = System.currentTimeMillis();" + rt + " System.out.println(\"starttime: \" + start);" + rt + " moveable.move();" + rt + " long end = System.currentTimeMillis();" + rt + " System.out.println(\"time: \" + (end - start));" + rt + " }" + rt + "}"; return null; } }
String src声明了一段源码,假设这段源码能够编译,产生一个新的类,再把这个类load到内存,用这个类产生一个对象;这个对象就是实现了TimeProxy逻辑的对象。
问题就变成了,如何把这段代码动态的编译。原来的TankTimeProxy类就不需要了,这个类的名字不重要,调用Proxy.newProxyInstance()方法就能返回一个具体的代理对象。
如何动态地编译这段String src的源码呢?有这些:JDK6 Compiler API,CGLib,ASM。JavaCompiler:Java编译器。CGLib:Code Generation Library是Code生产类库,它可以在运行期扩展Java类与实现Java接口。ASM:ASM是一个Java字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。ASM从类文件中读取信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
3)现在解决编译这段String src源代码的问题。
下面测试程序是来编译string src这样源码的。
com.sj.compiler.test.Test1
问题1、Usage of API documented as @since 1.6+
File ->Project Structure->Project Settings -> Modules -> 你的Module名字 -> Sources -> Language Level->选个默认的就行。
问题2、Error:java: Compilation failed: internal java compiler error
修改完pom.xml,需要Reimport才生效。
问题3、Exception in thread "main" java.lang.ClassNotFoundException: com.sj.proxy.TankTimeProxy
在 urlClassLoader.loadClass 打个断点,debug一次就可以了。
什么原因导致的,目前不知道。如果哪个大神知道,请留言告诉我。感谢!
package com.sj.compiler.test; import com.sj.proxy.Moveable; import com.sj.proxy.Tank; 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.lang.reflect.Constructor; import java.net.URL; import java.net.URLClassLoader; public class Test1 { public static void main(String[] args) throws Exception { String rt = "\r\n"; String src = "package com.sj.proxy;" + rt + "public class TankTimeProxy implements Moveable {" + rt + " private Moveable moveable;" + rt + " public TankTimeProxy(Moveable moveable) {" + rt + " this.moveable = moveable;" + rt + " }" + rt + " public void move() {" + rt + " long start = System.currentTimeMillis();" + rt + " System.out.println(\"starttime: \" + start);" + rt + " moveable.move();" + rt + " long end = System.currentTimeMillis();" + rt + " System.out.println(\"time: \" + (end - start));" + rt + " }" + rt + "}"; String fileUrl = System.getProperty("user.dir") + "/src/main/java/com/sj/proxy"; //user.dir:当前项目的根路径 //先把源代码写到下面 String fileName = fileUrl + "/TankTimeProxy.java"; File file = new File(fileName); FileWriter fileWriter = new FileWriter(file); fileWriter.write(src); fileWriter.flush(); fileWriter.close(); //在程序中编译这段代码 //JavaCompiler java编译器 //ToolProvider.getSystemJavaCompiler() 拿到系统当前默认的java编译器,其实就是javac JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager javaFileManager = compiler.getStandardFileManager(null, null, null); //通过javaFileManager管理要编译的文件 Iterable<? extends JavaFileObject> compilationUnits = javaFileManager.getJavaFileObjects(fileName); //拿到编译的内容 JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, javaFileManager, null, null, null, compilationUnits); //编译的任务 compilationTask.getClass(); //编译 javaFileManager.close(); //生成了这个TankTimeProxy.class之后,将期load到内存,并且生成一个对象 URL[] urls = new URL[]{new URL("file:/" + fileUrl)}; URLClassLoader urlClassLoader = new URLClassLoader(urls); Class clazz = urlClassLoader.loadClass("com.sj.proxy.TankTimeProxy"); //clazz.newInstance();//调用的是类的参数为空的构造方法,但是类中没有此构造方法。 //站在java虚拟机的角度,每一个类、每一个类里面的每一个方法都是一个对象。 Constructor constructor = clazz.getConstructor(Moveable.class); Moveable m = (Moveable) constructor.newInstance(new Tank()); m.move(); } }
打印结果如下:
starttime: 1514873531442
Tank Moving...
time: 3308
Proxy中就可以通过Proxy.newProxyInstance()来返回一个代理对象了。
4)现在确实能产生一个动态的代理了,但是现在产生的代理只能代理实现了Moveable接口的这样的一种代理;如果是实现了别的接口的就不行了,现在考虑能生产实现任意接口的代理类:
要做的事情:
1、在Proxy.newProxyInstance()方法中添加参数,Proxy.newProxyInstance(Class infce),将要实现的接口类型传进去,src源码中动态implements这个接口;
2、要知道infce中有多少个方法,src中就要动态实现这些方法,在这些方法中添加逻辑;
下面是代码实现:(没有模拟方法的参数和返回值)
a、测试程序,利用反射,获取接口中的方法:
public class Test2 { public static void main(String[] args) { //通过反射可以获取接口中有多少个方法 Method[] methods = com.sj.proxy.Moveable.class.getMethods(); for (Method m : methods) { System.out.println(m);//public abstract void com.sj.proxy.Moveable.move() } } }
b、代码实现,代理实现任意接口的类:
Proxy.java: newProxyInstance(Class infce):
package com.sj.proxy; 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.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; public class Proxy2 { //用来产生新的代理类的对象 public static Object newProxyInstance(Class infce) throws Exception { String rt = "\r\n"; String methodStr = ""; Method[] methods = infce.getMethods(); for (Method m : methods) { methodStr += " 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 + " }" + rt; } String src = "package com.sj.proxy;" + rt + "public class TankTimeProxy implements " + infce.getName() + " {" + rt + " private Moveable t;" + rt + " public TankTimeProxy(Moveable t) {" + rt + " this.t = t;" + rt + " }" + rt + methodStr + "}"; // String fileUrl = System.getProperty("user.dir") + "/src/main/java/com/sj/proxy"; //user.dir:当前项目的根路径 //先把源代码写到下面 // String fileName = fileUrl + "/TankTimeProxy.java"; String fileName = "d:/src/com/sj/proxy/TankTimeProxy.java"; File file = new File(fileName); FileWriter fileWriter = new FileWriter(file); fileWriter.write(src); fileWriter.flush(); fileWriter.close(); //在程序中编译这段代码 //JavaCompiler java编译器 //ToolProvider.getSystemJavaCompiler() 拿到系统当前默认的java编译器,其实就是javac JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager javaFileManager = compiler.getStandardFileManager(null, null, null); //通过javaFileManager管理要编译的文件 Iterable<? extends JavaFileObject> compilationUnits = javaFileManager.getJavaFileObjects(fileName); //拿到编译的内容 JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, javaFileManager, null, null, null, compilationUnits); //编译的任务 compilationTask.getClass(); //编译 javaFileManager.close(); //生成了这个TankTimeProxy.class之后,将期load到内存,并且生成一个对象 // URL[] urls = new URL[]{new URL("file:/" + fileUrl)}; URL[] urls = new URL[] {new URL("file:/" + "d:/src/")}; URLClassLoader urlClassLoader = new URLClassLoader(urls); Class clazz = urlClassLoader.loadClass("com.sj.proxy.TankTimeProxy"); //clazz.newInstance();//调用的是类的参数为空的构造方法,但是类中没有此构造方法。 //站在java虚拟机的角度,每一个类、每一个类里面的每一个方法都是一个对象。 Constructor constructor = clazz.getConstructor(infce); Object m = constructor.newInstance(new Tank()); return m; } }
//上面的代码中修改了生成源码文件、编译class文件的存放位置;
// 利用反射,动态生成实现接口方法的字符串;为了简单起见,没有模拟方法的参数和返回值;
c、测试代码
package com.sj.test;
import com.sj.proxy.Moveable;
import com.sj.proxy.Proxy2;
public class ProxyTest {
public static void main(String[] args) throws Exception {
Moveable m = (Moveable) Proxy2.newProxyInstance(Moveable.class);
m.move();
}
}
问题1、系统找不到指定的路径。
自己手动创建文件目录
打印结果如下:
starttime: 1515052644972
Tank Moving...
time: 3944
5)我们现在的Proxy能够对实现任意接口的类进行处理了,但是上面的Proxy中的生成逻辑的代码是固定的,只能是记录时间的代码,如果要生成一个记录日志的代理、权限的代理,就不行了;
现在怎么让这段内容,也能让用户灵活的指定呢?
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
……
也能让用户灵活地指定呢?
最好是这段代码能够调用别人指定的处理方式。现在需要一个能够调用别人指定的处理方法的东西。
调用别人的处理方法的东西:InvocationHandler.java
InvocationHandler定义了一个接口,这个接口可以对方法进行处理,处理的方式由子类去实现。
package com.sj.proxy; import java.lang.reflect.Method; public interface InvocationHandler { void invoke(Object o, Method m); //假设返回值void }
TimeHandler1.java
package com.sj.proxy; import java.lang.reflect.Method; public class TimeHandler1 { /** * 对方法进行自定义处理,在调用给定方法的前后加上记录时间的逻辑 * 方法的调用:对一个方法的调用,必须知道这个方法的当前对象是谁,也就是,this是谁 * * @param object 对哪个对象调用这个方法,即对object这个对象调用method方法 * @param method 想调用的那个方法,在调用这个方法的之前或之后做一些事情 */ public void invoke(Object object, Method method) { long start = System.currentTimeMillis(); System.out.println("starttime: " + start); try { method.invoke(object, new Object[]{}); //假设方法中没有参数 } catch (Exception e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } }
现在考虑Proxy中动态的代码该怎么生成了:
Proxy.java:
package com.sj.proxy; 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.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; public class Proxy { /** * 用来产生新的代理类 * * @param infce 产生哪个接口的动态代理 * @param handler 要实现什么样的代理,对接口中的方法前后进行什么样的处理,记录时间or... * @return * @throws Exception */ public static Object newProxyInstance(Class infce, InvocationHandler handler) throws Exception { String rt = "\r\n"; String methodStr = ""; Method[] methods = infce.getMethods(); for (Method m : methods) { methodStr += " public void " + m.getName() + "() {" + rt + " try {" + rt + " Method md = " + infce.getName() + ".class.getMethod(\"" + m.getName() + "\");" + rt + " h.invoke(this, md);" + rt + " } catch (Exception e) {" + rt + " e.printStackTrace();" + rt + " }" + rt + " }" + rt; } String src = "package com.sj.proxy;" + rt + "import java.lang.reflect.Method;" + rt + "public class TankTimeProxy implements " + infce.getName() + " {" + rt + " com.sj.proxy.InvocationHandler h;" + rt + " public TankTimeProxy(InvocationHandler h) {" + rt + " this.h = h;" + rt + " }" + rt + methodStr + "}"; String fileUrl = System.getProperty("user.dir") + "/src/main/java/com/sj/proxy"; String fileName = fileUrl + "/TankTimeProxy.java"; File file = new File(fileName); FileWriter fileWriter = new FileWriter(file); fileWriter.write(src); fileWriter.flush(); fileWriter.close(); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager javaFileManager = compiler.getStandardFileManager(null, null, null); Iterable<? extends JavaFileObject> compilationUnits = javaFileManager.getJavaFileObjects(fileName); JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, javaFileManager, null, null, null, compilationUnits); compilationTask.call(); javaFileManager.close(); URL[] urls = new URL[]{new URL("file:/" + fileUrl)}; URLClassLoader urlClassLoader = new URLClassLoader(urls); Class clazz = urlClassLoader.loadClass("com.sj.proxy.TankTimeProxy"); Constructor constructor = clazz.getConstructor(InvocationHandler.class); Object m = constructor.newInstance(handler); return m; } }
TimeHandler.java:
package com.sj.proxy; import java.lang.reflect.Method; public class TimeHandler implements InvocationHandler { private Object target;//被代理对象 public TimeHandler(Object target) { this.target = target; } /** * @param object o这里没用到。 * Proxy中的h.invoke(this, md),其实this没用到,this的指向是代理对象。 */ public void invoke(Object object, Method method) { long start = System.currentTimeMillis(); System.out.println("starttime: " + start); System.out.println(object.getClass().getName());//com.sj.proxy.TankTimeProxy try { method.invoke(target); //对被代理对象调用method方法 } catch (Exception e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("time: " + (end - start)); } }
Client.java测试代码:
package com.sj.test; import com.sj.proxy.*; public class Client { public static void main(String[] args) throws Exception { Tank t = new Tank();//被代理对象 InvocationHandler h = new TimeHandler(t);//代理逻辑,这里是记录时间的逻辑 //Proxy.newProxyInstance(Moveable.class, h);//返回一个代理对象,我们不知道名字 Moveable m = (Moveable) Proxy.newProxyInstance(Moveable.class, h); m.move(); } }
问题1、NoSuchMethodException
在Proxy 的 Class clazz = urlClassLoader.loadClass("com.sj.proxy.TankTimeProxy"); 打个断点,debug一次就可以了。
打印结果如下:
starttime: 1515136915379
com.sj.proxy.TankTimeProxy
Tank Moving...
time: 7152
看下生成的代理类——TankTimeProxy.java
package com.sj.proxy; import java.lang.reflect.Method; public class TankTimeProxy implements com.sj.proxy.Moveable { com.sj.proxy.InvocationHandler h; public TankTimeProxy(InvocationHandler h) { this.h = h; } public void move() { try { Method md = com.sj.proxy.Moveable.class.getMethod("move"); h.invoke(this, md); } catch (Exception e) { e.printStackTrace(); } } }
5、JDK中的动态代理,和上面模拟的代码几乎一样:
Proxy:
返回一个指定接口的代理类的实例。该代理类实例将方法调用指派给指定的调用处理程序。
ClassLoader:当你产生这个动态代理的时候,用哪个classLoader把它load进来;上面模拟是用的URLClassLoader;真正调的时候,可以用当前这个类的ClassLoader;如果用传过来的ClassLoader,得保证生成代理的类生成在特定的目录里面,才load进来。
java.lang.reflect.InvocationHandler:
在代理实例上处理方法调用,并返回结果。
proxy、method和上面代码模拟的一样。args是method的参数。
6、使用JDK里面的Proxy、InvocationHandler写的动态代理的例子
需求:在userDao的save、delete方法的前后加上日志记录
UserDao.java
package com.sj.aop; public interface UserDao { int save(String user); boolean delete(String user); }
UserDaoImpl.java
package com.sj.aop; public class UserDaoImpl implements UserDao { public int save(String user) { System.out.println("add User: " + user); return 1; } public boolean delete(String user) { System.out.println("delete User: " + user); return true; } }
UserDaoLogHandler.java,在方法的前后加上记录日志的逻辑
package com.sj.aop; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class UserDaoLogHandler implements InvocationHandler { //target是被代理对象 private Object target; public UserDaoLogHandler(Object target) { this.target = target; } /** * 调用被代理对象实现接口UserDao的每个方法(ave,delete)都会调用invoke方法; * 1.先加自己的业务逻辑 * 2.再调用被代理对象的对应方法; * 3.再加自己的业务逻辑也行。 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + "----log start..."); /** * obj 是target调用method的返回值。也就是public int save(String)方法的返回值int * method 是要调用的被代理对象的方法 * args 是method方法的参数 */ Object obj = method.invoke(target, args); System.out.println(method.getName() + "----log end."); return obj;//执行target.save,obj是1;执行target.delete,obj是true。 } }
UserService.java 测试代码:
package com.sj.aop; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class UserService { /** * 1.构建被代理对象 userDao, * 2.把被代理对象交给handler, * 3.使用newProxyInstance产生代理对象,其中的参数: * 1)calssLoader:必须和被代理对象使用同一个calssLoader, * 2)被代理对象使用的接口,jdk生成的代理对象会使用同样的接口, * 3)代理对象调用方法的时候是由handler来处理 */ public static void main(String[] args) throws Exception { UserDao userDaoImpl = new UserDaoImpl(); InvocationHandler invocationHandler = new UserDaoLogHandler(userDaoImpl); UserDao proxy = (UserDao) Proxy.newProxyInstance(userDaoImpl.getClass().getClassLoader(), userDaoImpl.getClass().getInterfaces(), invocationHandler); proxy.save("小明"); proxy.delete("小明"); } /** * JDK中要给一个类实现动态代理:这个类必须实现一个接口,没有实现接口的类,JDK是产生不了动态代理的。 */ }
打印结果如下:
save----log start...
add User: 小明
save----log end.
delete----log start...
delete User: 小明
delete----log end.
被代理对象实现接口的每个方法,前后都加上了逻辑。
源码:https://gitee.com/SevenDayBabyface/Proxy.git
参考 马士兵设计模式-动态代理