摘要:很多框架底層都使用了的動態代理技術來實現的,比如大名鼎鼎的這篇文章將帶你一步一步揭開動態代理技術的神秘面紗。接下來客戶端就可以這樣使用了毫秒到目前為止,我們實現的類可以為任何接口生成代理類了,是不是很神奇。
? 動態代理是java語言中常用的設計模式,java在1.3版本以后也提供了動態代理技術,允許開發者在運行期間創建接口的代理對象。 很多框架底層都使用了java的動態代理技術來實現的,比如大名鼎鼎的springAOP;這篇文章將帶你一步一步揭開JDK動態代理技術的神秘面紗。
? 我們先來定義一個接口:
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包提供給我們用的,這時如果你還想統計下這個方法運行時間,又該怎么辦呢?至少有兩種方式可以來實現:
1、使用繼承,寫一個類繼承Plane,重寫fly方法,在調用父類的fly方法前后加上①②處的代碼,這樣就可以統計fly方法的執行時間了。
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; } }
2、使用聚合的方式,寫一個類PlaneTimerProxy2實現跟Plane一樣的接口,并且持有IFlyable的引用,當調用fly方法時,實際調用的是IFlyable的fly方法,這樣就可以在方法調用前后加上①②處的代碼統計fly方法的執行的時間。
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接口的類產生代理類,那么客戶端調用就可以這樣做:
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 MapbytesMap = new HashMap<>(); private static final AtomicInteger count = new AtomicInteger(); public static Object newProxyInstance(Class> intaface,InvocationHandler handler) { //代碼①處 String rn = " "; 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(); } } } } }
我來解釋下上面代碼的意思:
1、代碼①處,根據傳入的接口動態生成java代碼的字符串,類名取名為Proxy+序號,該類實現了傳入的接口,真正的方法調用將委托傳入InvocationHandler的實現類來實現。
2、代碼②處,將生成的java代碼的字符串寫入文件
3、代碼③處,真正的核心,動態編譯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的二進制文件。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71942.html
摘要:使用與的靜態代理不同,使用的動態代理,所謂的動態代理就是說框架不會去修改字節碼,而是在內存中臨時為方法生成一個對象,這個對象包含了目標對象的全部方法,并且在特定的切點做了增強處理,并回調原對象的方法。 AOP(Aspect Orient Programming),我們一般稱為面向方面(切面)編程,作為面向對象的一種補充,用于處理系統中分布于各個模塊的橫切關注點,比如事務管理、日志、緩存...
摘要:代理模式概念代理模式分為兩種,一種是靜態代理模式,一種是動態代理模式。面向切面的編程也是使用動態代理模式來實現的。 1.代理模式概念 代理模式分為兩種,一種是靜態代理模式,一種是動態代理模式。 靜態代理模式:在程序運行之前需要寫好代理類 動態代理模式:在程序運行期間動態生成代理類 2.動態代理的實現 動態代理實現的步驟: (1)寫一個代理類SubjectHandler實現Invoca...
摘要:動態代理有多種不同的用途,例如,數據庫連接和事務管理用于單元測試的動態模擬對象其他類似的方法攔截。調用序列和下面的流程類似單元測試動態對象模擬利用動態代理實現單元測試的動態存根代理和代理。框架把包裝成動態代理。 使用反射可以在運行時動態實現接口。這可以使用類java.lang.reflect.Proxy。這個類的名稱是我將這些動態接口實現稱之為動態代理的原因。動態代理有多種不同的用途,...
摘要:對于人類來說,字節碼文件的可讀性遠遠沒有代碼高。盡管如此,還是有一些杰出的程序員們創造出了可以用來直接編輯字節碼的框架,提供接口可以讓我們方便地操作字節碼文件,進行注入修改類的方法,動態創造一個新的類等等操作。 引子 在遙遠的希艾斯星球爪哇國塞沃城中,兩名年輕的程序員正在為一件事情苦惱,程序出問題了,一時看不出問題出在哪里,于是有了以下對話: Debug一下吧。 線上機器,沒開Debu...
閱讀 2542·2021-10-11 10:58
閱讀 1020·2019-08-29 13:58
閱讀 1661·2019-08-26 13:32
閱讀 830·2019-08-26 10:40
閱讀 3256·2019-08-26 10:18
閱讀 1756·2019-08-23 14:18
閱讀 1106·2019-08-23 10:54
閱讀 435·2019-08-22 18:39