動態代理是java語言中經常使用的設計模式,java在1.3版本之後也提供了動態代理技術,容許開發者在運行期間建立接口的代理對象。 不少框架底層都使用了java的動態代理技術來實現的,好比大名鼎鼎的springAOP;這篇文章將帶你一步一步揭開JDK動態代理技術的神祕面紗。java
咱們先來定義一個接口:spring
package com.yanghui.study.proxy; public interface IFlyable { int fly(int x,int y); }
再來一個實現類:設計模式
package com.yanghui.study.proxy; public class Plane implements IFlyable{ @Override public int fly(int x, int y) { int result = x * x + y * y; try { Thread.sleep(new Random().nextInt(700)); } catch (InterruptedException e) { e.printStackTrace(); } return result; } }
若是咱們要統計一下這個fly方法的運行時間,該怎麼作呢?很簡單,能夠修改源碼在方法fly方法裏面加上兩句代碼①、②,這樣就打印出方法的運行時間了,以下:框架
//省略沒必要要代碼...... public int fly(int x, int y) { long start = System.currentTimeMillis();//①記錄開始時間 int result = x * x + y * y; try { Thread.sleep(new Random().nextInt(700)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//②結束時間減去開始時間 return result; }
可是若是咱們沒有這個方法的源碼,這個類是別人寫好打好jar包提供給咱們用的,這時若是你還想統計下這個方法運行時間,又該怎麼辦呢?至少有兩種方式能夠來實現:dom
一、使用繼承,寫一個類繼承Plane,重寫fly方法,在調用父類的fly方法先後加上①②處的代碼,這樣就能夠統計fly方法的執行時間了。ide
package com.yanghui.study.proxy; public class PlaneTimerProxy1 extends Plane{ @Override public int fly(int x, int y) { long start = System.currentTimeMillis();//① int result = super.fly(x, y); System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//② return result; } }
二、使用聚合的方式,寫一個類PlaneTimerProxy2
實現跟Plane同樣的接口,而且持有IFlyable的引用,當調用fly方法時,實際調用的是IFlyable的fly方法,這樣就能夠在方法調用先後加上①②處的代碼統計fly方法的執行的時間。ui
public class PlaneTimerProxy2 implements IFlyable{ private IFlyable flyable; public PlaneTimerProxy2(IFlyable flyable) { this.flyable = flyable; } @Override public int fly(int x, int y) { long start = System.currentTimeMillis();//① int result = this.flyable.fly(x, y); System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒");//② return result; } }
這兩種方式均可以實現,那麼哪一種方式更好呢?答案是聚合的方式更好,爲何呢?想象一下,若是我還想實現更多的功能,好比給fly方法執行先後加上日誌,事務控制,權限控制,這時用繼承的方式你會須要新建更多的類來實現,可能你會想,聚合的實現方式不也是要新建更多的類來實現嗎?是的,可是若是我要你先記錄日誌再記錄時間,有若是我要你先記錄時間再記錄日誌,須要實現這樣隨意的組合的功能,繼承就顯得很麻煩了,而聚合的方式就會很靈活了。在思考下,若是想給不一樣類的100個方法記錄下時間和日誌,那麼你想一想看是否是要產生100個代理類呢?類的數量又在不停的膨脹了。若是咱們可以爲實現了某個接口的類動態生成代理類就行了?想法很好,先來新建一個類Proxy
,提供一個方法newProxyInstance
,這個方法能夠爲一個實現了IFlyable
接口的類產生代理類,那麼客戶端調用就能夠這樣作:this
package com.yanghui.study.proxy.custom; public class Client { public static void main(String[] args) { IFlyable flyable = (IFlyable)Proxy.newProxyInstance(); flyable.fly(1, 2); } }
那麼咱們如何在newProxyInstance
方法裏面動態的生成一個代理類呢?爲了模擬JDK的實現,先定義一個接口InvocationHandler
:設計
package com.yanghui.study.proxy.custom; import java.lang.reflect.Method; public interface InvocationHandler { Object invoke(Object proxy,Method method,Object[] args)throws Throwable; }
下面來個完整代碼:代理
public class Proxy { private static final Map<String,byte[]> bytesMap = new HashMap<>(); private static final AtomicInteger count = new AtomicInteger(); public static Object newProxyInstance(Class<?> intaface,InvocationHandler handler) { //代碼①處 String rn = "\r\n"; String className = "Proxy" + count.getAndIncrement(); String str = "package com.yanghui.study.proxy.custom;" + rn + "public class " + className + " implements " + intaface.getName() + "{" + rn + " private InvocationHandler handler;" + rn + " public " + className + "(InvocationHandler handler){" + rn + " this.handler=handler;" + rn + " }" + rn; String methodStr = ""; for(Method m : intaface.getMethods()) { methodStr = methodStr + " @Override" + rn + " public " + m.getReturnType().getName() + " " + m.getName() + "("; String parameterStr = ""; String psType = ""; String pname = ""; for(Parameter p : m.getParameters()) { parameterStr = parameterStr + p + ","; psType = psType + p.getType().getName() + ".class,"; pname = pname + p.getName() + ","; } if(!parameterStr.equals("")) { parameterStr = parameterStr.substring(0, parameterStr.length() - 1); } parameterStr = parameterStr + "){" + rn + " try{" + rn + " " + Method.class.getName() + " method = " + intaface.getName() + ".class.getDeclaredMethod(\"" + m.getName() + "\""; if(!psType.equals("")) { psType = psType.substring(0, psType.length() - 1); parameterStr = parameterStr + "," + psType + ");" + rn; }else { parameterStr = parameterStr + ");" + rn; } if(pname.length() > 0) { pname = pname.substring(0, pname.length() - 1); } String returnStr = ""; if(!"void".equals(m.getReturnType().getName())) { returnStr = returnStr + " return (" + m.getReturnType().getName() + ")"; } parameterStr = parameterStr + returnStr + "this.handler.invoke(this,method," + (pname.length() == 0 ? "null" : "new Object[]{" + pname + "}") + ");" + rn + " } catch (Throwable e) {" + rn + " throw new RuntimeException(e);" + rn + " }" + rn + " }" + rn; methodStr = methodStr + parameterStr; } String endStr = "}"; str = str + methodStr + endStr; String path = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/yanghui/study/proxy/custom/"; String fileStr = path + className + ".java"; //代碼②處 //寫入文件 writeToFile(fileStr, str); //代碼③處 //動態編譯 String className1 = "com.yanghui.study.proxy.custom." + className; return compileToFileAndLoadclass(className1, fileStr, handler); } /** * 從源文件到字節碼文件的編譯方式 * @param className * @param fileStr * @param handler * @return */ private static Object compileToFileAndLoadclass(String className,String fileStr,InvocationHandler handler) { //獲取系統Java編譯器 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //獲取Java文件管理器 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); //定義要編譯的源文件 File file = new File(fileStr); //經過源文件獲取到要編譯的Java類源碼迭代器,包括全部內部類,其中每一個類都是一個 JavaFileObject,也被稱爲一個彙編單元 Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file); //生成編譯任務 JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); //執行編譯任務 task.call(); try { fileManager.close(); } catch (IOException e) { e.printStackTrace(); } try { Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(className); Constructor<?> ct = c.getConstructor(InvocationHandler.class); Object object = ct.newInstance(handler); return object; } catch (Exception e) { throw new RuntimeException(e); } } private static void writeToFile(String file,String context) { FileWriter fw = null; try { fw = new FileWriter(new File(file)); fw.write(context); } catch (IOException e) { e.printStackTrace(); }finally { if(fw != null) { try { fw.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
我來解釋下上面代碼的意思:
一、代碼①處,根據傳入的接口動態生成java代碼的字符串,類名取名爲Proxy+序號,該類實現了傳入的接口,真正的方法調用將委託傳入InvocationHandler
的實現類來實現。
二、代碼②處,將生成的java代碼的字符串寫入文件
三、代碼③處,真正的核心,動態編譯2步生成的java文件,再經過classLoader把編譯生成的class文件加載進內存,而後反射建立實例。
接下來客戶端就能夠這樣使用了:
public class Client { public static void main(String[] args) { Plane plane = new Plane(); IFlyable flyable = (IFlyable)Proxy.newProxyInstance(IFlyable.class,new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.currentTimeMillis(); Object result = method.invoke(plane, args); System.out.println("time:" + (System.currentTimeMillis() - start) + "毫秒"); return result; } }); System.out.println(flyable.fly(1, 2)); } }
到目前爲止,咱們實現的Proxy
類能夠爲任何接口生成代理類了,是否是很神奇。固然咱們這裏只是模擬實現了JDk的動態代理,還有不少細節是沒有考慮的,有興趣的同窗能夠本身閱讀JDK源碼,相信您理解了其背後的原理後,看起來也不會太費力了。
擴展
在上面咱們實現了動態生成java文件,動態編譯java文件,須要把文件寫入磁盤,也會在java源文件的目錄生成編譯後的.class文件,那麼能夠不能夠只在內存中編譯加載呢?答案是能夠的,代碼以下(方法是Proxy
類下的方法):
/** * 從內存到內存的編譯方式 * @param className * @param code * @param handler * @return */ @SuppressWarnings({ "unchecked", "rawtypes" }) private static Object compileMemoryToMemoryAndLoadClass(String className,String code,InvocationHandler handler) { if(bytesMap.get(className) != null) { return loadClass(className, bytesMap.get(className), handler); } //獲取系統Java編譯器 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //獲取Java文件管理器 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); ForwardingJavaFileManager fjf = new ForwardingJavaFileManager(fileManager) { @Override public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException { if(kind == JavaFileObject.Kind.CLASS) { return new SimpleJavaFileObject(URI.create(""), JavaFileObject.Kind.CLASS) { public OutputStream openOutputStream() { return new FilterOutputStream(new ByteArrayOutputStream()) { public void close() throws IOException{ out.close(); ByteArrayOutputStream bos = (ByteArrayOutputStream) out; bytesMap.put(className, bos.toByteArray()); } }; } }; }else{ return super.getJavaFileForOutput(location, className, kind, sibling); } } }; SimpleJavaFileObject sourceJavaFileObject = new SimpleJavaFileObject(URI.create(className.replace('.', '/') + Kind.SOURCE.extension),JavaFileObject.Kind.SOURCE){ @Override public CharBuffer getCharContent(boolean b) { return CharBuffer.wrap(code); } }; //生成編譯任務 JavaCompiler.CompilationTask task = compiler.getTask(null, fjf, null, null, null, Arrays.asList(new JavaFileObject[] {sourceJavaFileObject})); //執行編譯任務 task.call(); try { fileManager.close(); fjf.close(); } catch (IOException e) { e.printStackTrace(); } return loadClass(className, bytesMap.get(className), handler); } private static Object loadClass(String className,byte[] bytes,InvocationHandler handler) { try { Class<?> c = new MyClassLoader(bytes).loadClass(className); Constructor<?> ct = c.getConstructor(InvocationHandler.class); Object object = ct.newInstance(handler); return object; } catch (Exception e) { throw new RuntimeException(e); } }
首先經過本身定義sourceJavaFileObject
類來加載java格式的字符串,經過ForwardingJavaFileManager
類來從新定義編譯文件的輸出行爲,這裏我直接寫入內存,用一個map(bytesMap)來保存,key就是類名,value就是編譯好的.class的二進制文件。