前言
java設計模式9:代理模式一文中,講到了動態代理,動態代理里面用到了一個類就是java.lang.reflect.PRoxy,這個類是根據代理內容為傳入的接口生成代理用的。本文就自己寫一個Proxy類出來,功能和java.lang.reflect.Proxy一樣,傳入接口、代理內容,生成代理。
拋磚引玉吧,個人覺得自己寫一些JDK里面的那些類挺好的,寫一遍和看一遍真的是兩個不同的概念,寫一遍既加深了對于這些類的理解、提升了自己的寫代碼水平,也可以在寫完之后對比一下自己的實現有哪些寫得不好、又有哪些沒考慮到的地方,這樣可以顯著地提高自己,像我就自己寫過JDK里面主要的集合類、工具類、String里面常用方法等。
本文的代碼基礎來源于馬士兵Proxy的視頻(順便說一句,個人覺得馬士兵的視頻講得比較拖拉,但是關于一些原理性、偏底層的東西講得還蠻好的),一共分三個版本。可能有人覺得,人家視頻上的內容拿過來寫個文章,有意思嗎?真不是,我是這么認為的:
1、把別人的東西變成自己的東西是一個過程,盡管代碼是基于馬士兵Proxy的視頻的,但是所有的代碼都是在自己這里手打、運行通過并自己充分理解了的,把別人的東西不加思考地復制黏貼沒有意義,但是把別人的知識變成自己的理解并分享我覺得是一件好事
2、代碼盡管基于馬士兵Proxy的基礎上,但在這個基礎上也是做了自己的優化過的
動態代理的實現應用到的技術
1、動態編譯技術,可以使用Java自帶的JavaCompiler類,也可以使用CGLIB、ASM等字節碼增強技術,Java的動態代理包括Spring的內部實現貌似用的都是這個
2、反射,包括對于類.class和getClass()方法的理解,Method類、Constructor類的理解
3、IO流,主要就是字符輸出流FileWriter
4、對于ClassLoader的理解
基礎類
先把基礎類定義在這兒,首先是一個HelloWorld接口:
public interface HelloWorld{ void print();}
HelloWorld接口的實現類:
public class HelloWorldImpl implements HelloWorld{ public void print() { System.out.println("Hello World"); }}
為這個接口寫一個簡單的靜態代理類:
public class StaticProxy implements HelloWorld{ private HelloWorld helloWorld; public StaticProxy(HelloWorld helloWorld) { this.helloWorld = helloWorld; } public void print() { System.out.println("Before Hello World!"); helloWorld.print(); System.out.println("After Hello World!"); }}
版本1:為一個靜態代理動態生成一個代理類
我們知道如果用靜態代理的話,那么每個接口都要為之寫一個.java的代理類,這樣就可能造成代理類無限膨脹,如果可以讓Java幫我們自動生成一個就好了,不過還真的可以,看下第一個版本的代碼:
1 public class ProxyVersion_0 implements Serializable 2 { 3 private static final long serialVersionUID = 1L; 4 5 public static Object newProxyInstance() throws Exception 6 { 7 String src = "package com.xrq.proxy;/n/n" + 8 "public class StaticProxy implements HelloWorld/n" + 9 "{/n" + 10 "/tHelloWorld helloWorld;/n/n" + 11 "/tpublic StaticProxy(HelloWorld helloWorld)/n" + 12 "/t{/n" + 13 "/t/tthis.helloWorld = helloWorld;/n" + 14 "/t}/n/n" + 15 "/tpublic void print()/n" + 16 "/t{/n" + 17 "/t/tSystem.out.println(/"Before Hello World!/");/n" + 18 "/t/thelloWorld.print();/n" + 19 "/t/tSystem.out.println(/"After Hello World!/");/n" + 20 "/t}/n" + 21 "}";22 23 /** 生成一段Java代碼 */24 String fileDir = System.getProperty("user.dir");25 String fileName = fileDir + "http://src//com//xrq//proxy//StaticProxy.java";26 File javaFile = new File(fileName);27 Writer writer = new FileWriter(javaFile);28 writer.write(src);29 writer.close();30 31 /** 動態編譯這段Java代碼,生成.class文件 */32 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();33 StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);34 Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);35 CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);36 ct.call();37 sjfm.close();38 39 /** 將生成的.class文件載入內存,默認的ClassLoader只能載入CLASSPATH下的.class文件 */40 URL[] urls = new URL[] {(new URL("file://" + System.getProperty("user.dir") + "http://src"))};41 URLClassLoader ul = new URLClassLoader(urls);42 Class<?> c = ul.loadClass("com.xrq.proxy.StaticProxy");43 44 /** 利用反射將c實例化出來 */45 Constructor<?> constructor = c.getConstructor(HelloWorld.class);46 HelloWorld helloWorldImpl = new HelloWorldImpl();47 HelloWorld helloWorld = (HelloWorld)constructor.newInstance(helloWorldImpl);48 49 /** 使用完畢刪除生成的代理.java文件和.class文件,這樣就看不到動態生成的內容了 */50 File classFile = new File(fileDir + "http://src//com//xrq//proxy//StaticProxy.class");51 javaFile.delete();52 classFile.delete();53 54 return helloWorld;55 }56 }
每一步的注釋都在上面了,解釋一下大致思路:
1、我們在另外一個類里面自己拼一段靜態代理的代碼的字符串
2、為這個字符串生成一個.java文件,并放在我們工程的某個目錄下面,因為是.java文件,所以在src下
3、利用JavaCompiler類動態編譯這段.java代碼使之被編譯成一個.class文件,JavaCompiler不熟悉沒關系,知道就好了
4、因為在src下生成編譯之后的.java文件,而默認的ClassLoader只能加載CLASSPATH下的.class文件,所以用URLClassLoader
5、由于代理類只有一個帶參數的構造方法,所以要用java.lang.reflect.Constructor
6、最后把生成的StaticProxy.class文件刪除(最好生成的StaticProxy.java也刪除,這里沒刪除,是因為StaticProxy是生成的一個重要的中間類,功能都在它這兒,所以不刪,出了錯都要靠看這個類來定位問題的),這樣代理的中間內容都沒了,把反射newInstance()出來的內容返回出去就大功告成了
可以自己看一下生成的StaticProxy.java對不對,寫一段代碼測試一下:
public static void main(String[] args) throws Exception{ long start = System.currentTimeMillis(); HelloWorld helloWorld = (HelloWorld)ProxyVersion_0.newProxyInstance(); System.out.println("動態生成代理耗時:" + (System.currentTimeMillis() - start) + "ms"); helloWorld.print(); System.out.println(); }
結果為:
動態生成代理耗時:387msBefore Hello World!Hello WorldAfter Hello World!
沒有問題??赡苡行┤诉\行會報錯"Exception in thread "main" java.lang.ClassNotFoundException: com.xrq.proxy.StaticProxy",沒關系,那是因為雖然你的src目錄下生成了StaticProxy.class,但沒有出來,點擊src文件夾,再按F5(或者右鍵,點擊Refresh也行)刷新一下就可以了
版本二:為指定接口生成代理類
版本一已經實現了動態生成一個代理的.class文件了,算是成功的第一步,接下來要做進一步的改進。版本一只可以為固定的一個接口生成代理,現在改進成,傳入某個接口的java.lang.Class對象,可以為這個接口及里面的方法都生成代理內容,代碼這么寫:
1 public class ProxyVersion_1 implements Serializable 2 { 3 private static final long serialVersionUID = 1L; 4 5 public static Object newProxyInstance(Class<?> interfaces) throws Exception 6 { 7 Method[] methods = interfaces.getMethods(); 8 9 StringBuilder sb = new StringBuilder(700);10 11 sb.append("package com.xrq.proxy;/n/n");12 sb.append("public class StaticProxy implements " + interfaces.getSimpleName() + "/n");13 sb.append("{/n");14 sb.append("/t" + interfaces.getSimpleName() + " interfaces;/n/n");15 sb.append("/tpublic StaticProxy(" + interfaces.getSimpleName() + " interfaces)/n");16 sb.append("/t{/n");17 sb.append("/t/tthis.interfaces = interfaces;/n");18 sb.append("/t}/n/n");19 for (Method m : methods)20 {21 sb.append("/tpublic " + m.getReturnType() + " " + m.getName() + "()/n");22 sb.append("/t{/n");23 sb.append("/t/tSystem.out.println(/"Before Hello World!/");/n");24 sb.append("/t/tinterfaces." + m.getName() + "();/n");25 sb.append("/t/tSystem.out.println(/"After Hello World!/");/n");26 sb.append("/t}/n");27 }28 sb.append("}");29 30 /** 生成一段Java代碼 */31 String fileDir = System.getProperty("user.dir");32 String fileName = fileDir + "http://src//com//xrq//proxy//StaticProxy.java";33 File javaFile = new File(fileName);34 Writer writer = new FileWriter(javaFile);35 writer.write(sb.toString());36 writer.close();37 38 /** 動態編譯這段Java代碼,生成.class文件 */39 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();40 StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);41 Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);42 CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);43 ct.call();44 sjfm.close();45 46 /** 將生成的.class文件載入內存,默認的ClassLoader只能載入CLASSPATH下的.class文件 */47 URL[] urls = new URL[] {(new URL("file://" + System.getProperty("user.dir") + "http://src"))};48 URLClassLoader ul = new URLClassLoader(urls);49 Class<?> c = ul.loadClass("com.xrq.proxy.StaticProxy");50 51 /** 利用反射將c實例化出來 */52 Constructor<?> constructor = c.getConstructor(HelloWorld.class);53 HelloWorld helloWorldImpl = new HelloWorldImpl();54 Object obj = constructor.newInstance(helloWorldImpl);55 56 /** 使用完畢刪除生成的代理.java文件和.class文件,這樣就看不到動態生成的內容了 */57 /*File classFile = new File(fileDir + "http://src//com//xrq//proxy//StaticProxy.class");58 javaFile.delete();59 classFile.delete();*/60 61 return obj;62 }63 }
看到下面都沒有變化,變化的地方就是在生成StaticProxy.java的地方,通過反射獲取接口及方法的信息,這個版本的改進應該很好理解,寫一段代碼測試一下:
public static void main(String[] args) throws Exception{ long start = System.currentTimeMillis(); HelloWorld helloWorld = (HelloWorld)ProxyVersion_1.newProxyInstance(HelloWorld.class); System.out.println("動態生成代理耗時:" + (System.currentTimeMillis() - start) + "ms"); helloWorld.print(); System.out.println();}
運行結果為:
動態生成代理耗時:389msBefore Hello World!Hello WorldAfter Hello World!
也沒有問題
版本三:讓代理內容可復用
接下來要到最后一個版本了,版本二解決的問題是可以為任何接口生成代理,那最后一個版本要解決的問題自然是可以為任何接口生成任何代理的問題了,首先定義一個接口InvocationHandler,這么起名字是因為JDK提供的代理實例處理程序的接口也是InvocationHandler:
public interface InvocationHandler{ void invoke(Object proxy, Method method) throws Exception;}
所以我們的Proxy類也要修改了,改為:
1 public class ProxyVersion_2 implements Serializable 2 { 3 private static final long serialVersionUID = 1L; 4 5 public static Object newProxyInstance(Class<?> interfaces, InvocationHandler h) throws Exception 6 { 7 Method[] methods = interfaces.getMethods(); 8 StringBuilder sb = new StringBuilder(1024); 9 10 sb.append("package com.xrq.proxy;/n/n");11 sb.append("import java.lang.reflect.Method;/n/n");12 sb.append("public class $Proxy1 implements " + interfaces.getSimpleName() + "/n");13 sb.append("{/n");14 sb.append("/tInvocationHandler h;/n/n");15 sb.append("/tpublic $Proxy1(InvocationHandler h)/n");16 sb.append("/t{/n");17 sb.append("/t/tthis.h = h;/n");18 sb.append("/t}/n/n");19 for (Method m : methods)20 {21 sb.append("/tpublic " + m.getReturnType() + " " + m.getName() + "()/n");22 sb.append("/t{/n");23 sb.append("/t/ttry/n");24 sb.append("/t/t{/n");25 sb.append("/t/t/tMethod md = " + interfaces.getName() + ".class.getMethod(/"" + m.getName() + "/");/n");26 sb.append("/t/t/th.invoke(this, md);/n");27 sb.append("/t/t}/n");28 sb.append("/t/tcatch (Exception e)/n");29 sb.append("/t/t{/n");30 sb.append("/t/t/te.printStackTrace();/n");31 sb.append("/t/t}/n");32 sb.append("/t}/n");33 }34 sb.append("}");35 36 /** 生成一段Java代碼 */37 String fileDir = System.getProperty("user.dir");38 String fileName = fileDir + "http://src//com//xrq//proxy//$Proxy1.java";39 File javaFile = new File(fileName);40 Writer writer = new FileWriter(javaFile);41 writer.write(sb.toString());42 writer.close();43 44 /** 動態編譯這段Java代碼,生成.class文件 */45 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();46 StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);47 Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);48 CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);49 ct.call();50 sjfm.close();51 52 /** 將生成的.class文件載入內存,默認的ClassLoader只能載入CLASSPATH下的.class文件 */53 URL[] urls = new URL[] {(new URL("file://" + System.getProperty("user.dir") + "http://src"))};54 URLClassLoader ul = new URLClassLoader(urls);55 Class<?> c = Class.forName("com.xrq.proxy.$Proxy1", false, ul);56 57 /** 利用反射將c實例化出來 */58 Constructor<?> constructor = c.getConstructor(InvocationHandler.class);59 Object obj = constructor.newInstance(h);60 61 /** 使用完畢刪除生成的代理.java文件和.class文件,這樣就看不到動態生成的內容了 */62 File classFile = new File(fileDir + "http://src//com//xrq//proxy//$Proxy1.class");63 javaFile.delete();64 classFile.delete();65 66 return obj;67 }68 }
最明顯的變化,代理的名字變了,從StaticProxy變成了$Proxy1,因為JDK也是這么命名的,用過代理的應該有印象。這個改進中拼接$Proxy1的.java文件是一個難點,不過我覺得可以不用糾結在這里,關注重點,看一下生成的$Proxy1.java的內容是什么:
public class $Proxy1 implements HelloWorld{ InvocationHandler h; public $Proxy1(InvocationHandler h) { this.h = h; } public void print() { try { Method md = com.xrq.proxy.HelloWorld.class.getMethod("print"); h.invoke(this, md); } catch (Exception e) { e.printStackTrace(); } }}
看到,我們把對于待生成代理的接口方法的調用,變成了對于InvocationHandler接口實現類的invoke方法的調用(這就是動態代理最關鍵的一點),并傳入了待調用的接口方法,這樣不就實現了我們的要求了嗎?我們InvocationHandler接口的實現類寫invoke方法的具體實現,傳入的第二個參數md.invoke就是調用被代理對象的方法,在這個方法前后都是代理內容,想加什么加什么,不就實現了動態代理了?所以,我們看一個InvocationHandler實現類的寫法:
public class HelloInvocationHandler implements InvocationHandler{ private Object obj; public HelloInvocationHandler(Object obj) { this.obj = obj; } public void invoke(Object proxy, Method method) { System.out.println("Before Hello World!"); try { method.invoke(obj, new Object[]{}); } catch (Exception e) { e.printStackTrace(); } System.out.println("After Hello World!"); }}
寫個main函數測試一下:
public static void main(String[] args) throws Exception{ long start = System.currentTimeMillis(); HelloWorld helloWorldImpl = new HelloWorldImpl(); InvocationHandler ih = new HelloInvocationHandler(helloWorldImpl); HelloWorld helloWorld = (HelloWorld)ProxyVersion_2.newProxyInstance(HelloWorld.class, ih); System.out.println("動態生成代理耗時:" + (System.currentTimeMillis() - start) + "ms"); helloWorld.print(); System.out.println();}
運行結果為:
動態生成代理耗時:351msBefore Hello World!Hello WorldAfter Hello World!
沒有問題
后記
雖然我們自己寫了Proxy,但是JDK絕對不會用這種方式實現,原因無他,就是太慢。看到三個版本的代碼,運行時間都在300ms以上,效率如此低的實現,如何能給開發者使用?我拿JDK提供的Proxy和InvocationHandler自己寫了一個簡單的動態代理,耗時基本只在5ms左右。所以,文章的內容僅供學習、研究,知識點很多,如果能把這篇文章里面的東西都弄懂,對于個人水平、對于Java很多知識點的理解,絕對是一個非常大的提高。
新聞熱點
疑難解答