摘要:很多框架底層都使用了的動態(tài)代理技術(shù)來實現(xiàn)的,比如大名鼎鼎的這篇文章將帶你一步一步揭開動態(tài)代理技術(shù)的神秘面紗。接下來客戶端就可以這樣使用了毫秒到目前為止,我們實現(xiàn)的類可以為任何接口生成代理類了,是不是很神奇。
? 動態(tài)代理是java語言中常用的設(shè)計模式,java在1.3版本以后也提供了動態(tài)代理技術(shù),允許開發(fā)者在運(yùn)行期間創(chuàng)建接口的代理對象。 很多框架底層都使用了java的動態(tài)代理技術(shù)來實現(xiàn)的,比如大名鼎鼎的springAOP;這篇文章將帶你一步一步揭開JDK動態(tài)代理技術(shù)的神秘面紗。
? 我們先來定義一個接口:
package com.yanghui.study.proxy; public interface IFlyable { int fly(int x,int y); }
再來一個實現(xiàn)類:
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; } }
如果我們要統(tǒng)計一下這個fly方法的運(yùn)行時間,該怎么做呢?很簡單,可以修改源碼在方法fly方法里面加上兩句代碼①、②,這樣就打印出方法的運(yùn)行時間了,如下:
//省略不必要代碼...... 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) + "毫秒");//②結(jié)束時間減去開始時間 return result; }
但是如果我們沒有這個方法的源碼,這個類是別人寫好打好jar包提供給我們用的,這時如果你還想統(tǒng)計下這個方法運(yùn)行時間,又該怎么辦呢?至少有兩種方式可以來實現(xiàn):
1、使用繼承,寫一個類繼承Plane,重寫fly方法,在調(diào)用父類的fly方法前后加上①②處的代碼,這樣就可以統(tǒng)計fly方法的執(zhí)行時間了。
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實現(xiàn)跟Plane一樣的接口,并且持有IFlyable的引用,當(dāng)調(diào)用fly方法時,實際調(diào)用的是IFlyable的fly方法,這樣就可以在方法調(diào)用前后加上①②處的代碼統(tǒng)計fly方法的執(zhí)行的時間。
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; } }
這兩種方式都可以實現(xiàn),那么哪種方式更好呢?答案是聚合的方式更好,為什么呢?想象一下,如果我還想實現(xiàn)更多的功能,比如給fly方法執(zhí)行前后加上日志,事務(wù)控制,權(quán)限控制,這時用繼承的方式你會需要新建更多的類來實現(xiàn),可能你會想,聚合的實現(xiàn)方式不也是要新建更多的類來實現(xiàn)嗎?是的,但是如果我要你先記錄日志再記錄時間,有如果我要你先記錄時間再記錄日志,需要實現(xiàn)這樣隨意的組合的功能,繼承就顯得很麻煩了,而聚合的方式就會很靈活了。在思考下,如果想給不同類的100個方法記錄下時間和日志,那么你想想看是不是要產(chǎn)生100個代理類呢?類的數(shù)量又在不停的膨脹了。如果我們能夠為實現(xiàn)了某個接口的類動態(tài)生成代理類就好了?想法很好,先來新建一個類Proxy,提供一個方法newProxyInstance,這個方法可以為一個實現(xiàn)了IFlyable接口的類產(chǎn)生代理類,那么客戶端調(diào)用就可以這樣做:
package com.yanghui.study.proxy.custom; public class Client { public static void main(String[] args) { IFlyable flyable = (IFlyable)Proxy.newProxyInstance(); flyable.fly(1, 2); } }
那么我們?nèi)绾卧?b>newProxyInstance方法里面動態(tài)的生成一個代理類呢?為了模擬JDK的實現(xiàn),先定義一個接口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); //代碼③處 //動態(tài)編譯 String className1 = "com.yanghui.study.proxy.custom." + className; return compileToFileAndLoadclass(className1, fileStr, handler); } /** * 從源文件到字節(jié)碼文件的編譯方式 * @param className * @param fileStr * @param handler * @return */ private static Object compileToFileAndLoadclass(String className,String fileStr,InvocationHandler handler) { //獲取系統(tǒng)Java編譯器 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //獲取Java文件管理器 StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); //定義要編譯的源文件 File file = new File(fileStr); //通過源文件獲取到要編譯的Java類源碼迭代器,包括所有內(nèi)部類,其中每個類都是一個 JavaFileObject,也被稱為一個匯編單元 Iterable extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file); //生成編譯任務(wù) JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); //執(zhí)行編譯任務(wù) 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、代碼①處,根據(jù)傳入的接口動態(tài)生成java代碼的字符串,類名取名為Proxy+序號,該類實現(xiàn)了傳入的接口,真正的方法調(diào)用將委托傳入InvocationHandler的實現(xiàn)類來實現(xiàn)。
2、代碼②處,將生成的java代碼的字符串寫入文件
3、代碼③處,真正的核心,動態(tài)編譯2步生成的java文件,再通過classLoader把編譯生成的class文件加載進(jìn)內(nèi)存,然后反射創(chuàng)建實例。
接下來客戶端就可以這樣使用了:
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)); } }
到目前為止,我們實現(xiàn)的Proxy類可以為任何接口生成代理類了,是不是很神奇。當(dāng)然我們這里只是模擬實現(xiàn)了JDk的動態(tài)代理,還有很多細(xì)節(jié)是沒有考慮的,有興趣的同學(xué)可以自己閱讀JDK源碼,相信您理解了其背后的原理后,看起來也不會太費(fèi)力了。
擴(kuò)展
在上面我們實現(xiàn)了動態(tài)生成java文件,動態(tài)編譯java文件,需要把文件寫入磁盤,也會在java源文件的目錄生成編譯后的.class文件,那么可以不可以只在內(nèi)存中編譯加載呢?答案是可以的,代碼如下(方法是Proxy類下的方法):
/** * 從內(nèi)存到內(nèi)存的編譯方式 * @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); } //獲取系統(tǒng)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); } }; //生成編譯任務(wù) JavaCompiler.CompilationTask task = compiler.getTask(null, fjf, null, null, null, Arrays.asList(new JavaFileObject[] {sourceJavaFileObject})); //執(zhí)行編譯任務(wù) 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類來重新定義編譯文件的輸出行為,這里我直接寫入內(nèi)存,用一個map(bytesMap)來保存,key就是類名,value就是編譯好的.class的二進(jìn)制文件。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/71942.html
摘要:使用與的靜態(tài)代理不同,使用的動態(tài)代理,所謂的動態(tài)代理就是說框架不會去修改字節(jié)碼,而是在內(nèi)存中臨時為方法生成一個對象,這個對象包含了目標(biāo)對象的全部方法,并且在特定的切點(diǎn)做了增強(qiáng)處理,并回調(diào)原對象的方法。 AOP(Aspect Orient Programming),我們一般稱為面向方面(切面)編程,作為面向?qū)ο蟮囊环N補(bǔ)充,用于處理系統(tǒng)中分布于各個模塊的橫切關(guān)注點(diǎn),比如事務(wù)管理、日志、緩存...
摘要:代理模式概念代理模式分為兩種,一種是靜態(tài)代理模式,一種是動態(tài)代理模式。面向切面的編程也是使用動態(tài)代理模式來實現(xiàn)的。 1.代理模式概念 代理模式分為兩種,一種是靜態(tài)代理模式,一種是動態(tài)代理模式。 靜態(tài)代理模式:在程序運(yùn)行之前需要寫好代理類 動態(tài)代理模式:在程序運(yùn)行期間動態(tài)生成代理類 2.動態(tài)代理的實現(xiàn) 動態(tài)代理實現(xiàn)的步驟: (1)寫一個代理類SubjectHandler實現(xiàn)Invoca...
摘要:動態(tài)代理有多種不同的用途,例如,數(shù)據(jù)庫連接和事務(wù)管理用于單元測試的動態(tài)模擬對象其他類似的方法攔截。調(diào)用序列和下面的流程類似單元測試動態(tài)對象模擬利用動態(tài)代理實現(xiàn)單元測試的動態(tài)存根代理和代理??蚣馨寻b成動態(tài)代理。 使用反射可以在運(yùn)行時動態(tài)實現(xiàn)接口。這可以使用類java.lang.reflect.Proxy。這個類的名稱是我將這些動態(tài)接口實現(xiàn)稱之為動態(tài)代理的原因。動態(tài)代理有多種不同的用途,...
摘要:對于人類來說,字節(jié)碼文件的可讀性遠(yuǎn)遠(yuǎn)沒有代碼高。盡管如此,還是有一些杰出的程序員們創(chuàng)造出了可以用來直接編輯字節(jié)碼的框架,提供接口可以讓我們方便地操作字節(jié)碼文件,進(jìn)行注入修改類的方法,動態(tài)創(chuàng)造一個新的類等等操作。 引子 在遙遠(yuǎn)的希艾斯星球爪哇國塞沃城中,兩名年輕的程序員正在為一件事情苦惱,程序出問題了,一時看不出問題出在哪里,于是有了以下對話: Debug一下吧。 線上機(jī)器,沒開Debu...
閱讀 2556·2021-10-11 10:58
閱讀 1041·2019-08-29 13:58
閱讀 1675·2019-08-26 13:32
閱讀 840·2019-08-26 10:40
閱讀 3268·2019-08-26 10:18
閱讀 1764·2019-08-23 14:18
閱讀 1116·2019-08-23 10:54
閱讀 443·2019-08-22 18:39