Java動態代理原理及實現

     最近項目和看技術文章的時候接觸了點Java動態代理和cglib、asm等知識,發現對於動態代理整套機制理解不夠,總以爲是採取切片等方式,運行時利用反射,通過標記等在需代理方法或者接口等上下文中執行某種增強方法,未想到會有中間字節碼的動態生成,看到博主的這篇文章,覺得寫得比較仔細,描述清楚了組建代理對象、字節碼生成、執行代理功能等各個流程。見賢思齊,看到好的文章在此搬運過來沉澱傳播,自己寫一個java.lang.reflect.Proxy代理的實現  ,在此表示感謝!


前言

     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幫我們自動生成一個就好了,不過還真的可以,看下第一個版本的代碼:

public class ProxyVersion_0 implements Serializable
{
    private static final long serialVersionUID = 1L;
    
    public static Object newProxyInstance() throws Exception
    {
        String src = "package com.xrq.proxy;\n\n" + 
                     "public class StaticProxy implements HelloWorld\n" + 
                     "{\n" + 
                     "\tHelloWorld helloWorld;\n\n" + 
                     "\tpublic StaticProxy(HelloWorld helloWorld)\n" + 
                     "\t{\n" + 
                     "\t\tthis.helloWorld = helloWorld;\n" + 
                     "\t}\n\n" + 
                     "\tpublic void print()\n" + 
                     "\t{\n" + 
                     "\t\tSystem.out.println(\"Before Hello World!\");\n" + 
                     "\t\thelloWorld.print();\n" + 
                     "\t\tSystem.out.println(\"After Hello World!\");\n" + 
                     "\t}\n" + 
                     "}";
        
        /** 生成一段Java代碼 */
        String fileDir = System.getProperty("user.dir");
        String fileName = fileDir + "\\src\\com\\xrq\\proxy\\StaticProxy.java";
        File javaFile = new File(fileName);
        Writer writer = new FileWriter(javaFile);
        writer.write(src);
        writer.close();
        
        /** 動態編譯這段Java代碼,生成.class文件 */
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
        CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
        ct.call();
        sjfm.close();
        
        /** 將生成的.class文件載入內存,默認的ClassLoader只能載入CLASSPATH下的.class文件 */
        URL[] urls = new URL[] {(new URL("file:\\" + System.getProperty("user.dir") + "\\src"))};
        URLClassLoader ul = new URLClassLoader(urls);
        Class<?> c = ul.loadClass("com.xrq.proxy.StaticProxy");
        
        /** 利用反射將c實例化出來 */
        Constructor<?> constructor = c.getConstructor(HelloWorld.class);
        HelloWorld helloWorldImpl = new HelloWorldImpl();
        HelloWorld helloWorld = (HelloWorld)constructor.newInstance(helloWorldImpl);
        
        /** 使用完畢刪除生成的代理.java文件和.class文件,這樣就看不到動態生成的內容了 */
        File classFile = new File(fileDir + "\\src\\com\\xrq\\proxy\\StaticProxy.class");
        javaFile.delete();
        classFile.delete();
        
        return helloWorld;
    }
}


每一步的註釋都在上面了,解釋一下大致思路:
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();        
}

結果爲:

動態生成代理耗時:387ms
Before Hello World!
Hello World
After Hello World!


     沒有問題。可能有些人運行會報錯"Exception in thread "main" java.lang.ClassNotFoundException: com.xrq.proxy.StaticProxy",沒關係,那是因爲雖然你的src目錄下生成了StaticProxy.class,但沒有出來,點擊src文件夾,再按F5(或者右鍵,點擊Refresh也行)刷新一下就可以了


版本二:爲指定接口生成代理類
     版本一已經實現了動態生成一個代理的.class文件了,算是成功的第一步,接下來要做進一步的改進。版本一只可以爲固定的一個接口生成代理,現在改進成,傳入某個接口的java.lang.Class對象,可以爲這個接口及裏面的方法都生成代理內容,代碼這麼寫:

public class ProxyVersion_1 implements Serializable
{
    private static final long serialVersionUID = 1L;
    
    public static Object newProxyInstance(Class<?> interfaces) throws Exception
    {
        Method[] methods = interfaces.getMethods();
        
        StringBuilder sb = new StringBuilder(700);
        
        sb.append("package com.xrq.proxy;\n\n");
        sb.append("public class StaticProxy implements " +  interfaces.getSimpleName() + "\n");
        sb.append("{\n");
        sb.append("\t" + interfaces.getSimpleName() + " interfaces;\n\n");
        sb.append("\tpublic StaticProxy(" + interfaces.getSimpleName() +  " interfaces)\n");
        sb.append("\t{\n");
        sb.append("\t\tthis.interfaces = interfaces;\n");
        sb.append("\t}\n\n");
        for (Method m : methods)
        {
            sb.append("\tpublic " + m.getReturnType() + " " + m.getName() + "()\n");
            sb.append("\t{\n");
            sb.append("\t\tSystem.out.println(\"Before Hello World!\");\n");
            sb.append("\t\tinterfaces." + m.getName() + "();\n");
            sb.append("\t\tSystem.out.println(\"After Hello World!\");\n");
            sb.append("\t}\n");
        }
        sb.append("}");
        
        /** 生成一段Java代碼 */
        String fileDir = System.getProperty("user.dir");
        String fileName = fileDir + "\\src\\com\\xrq\\proxy\\StaticProxy.java";
        File javaFile = new File(fileName);
        Writer writer = new FileWriter(javaFile);
        writer.write(sb.toString());
        writer.close();
        
        /** 動態編譯這段Java代碼,生成.class文件 */
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
        CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
        ct.call();
        sjfm.close();
        
        /** 將生成的.class文件載入內存,默認的ClassLoader只能載入CLASSPATH下的.class文件 */
        URL[] urls = new URL[] {(new URL("file:\\" + System.getProperty("user.dir") + "\\src"))};
        URLClassLoader ul = new URLClassLoader(urls);
        Class<?> c = ul.loadClass("com.xrq.proxy.StaticProxy");
        
        /** 利用反射將c實例化出來 */
        Constructor<?> constructor = c.getConstructor(HelloWorld.class);
        HelloWorld helloWorldImpl = new HelloWorldImpl();
        Object obj = constructor.newInstance(helloWorldImpl);
        
        /** 使用完畢刪除生成的代理.java文件和.class文件,這樣就看不到動態生成的內容了 */
        /*File classFile = new File(fileDir + "\\src\\com\\xrq\\proxy\\StaticProxy.class");
        javaFile.delete();
        classFile.delete();*/
        
        return obj;
    }
}


     看到下面都沒有變化,變化的地方就是在生成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();
}

運行結果爲:

動態生成代理耗時:389ms
Before Hello World!
Hello World
After Hello World!

也沒有問題


版本三:讓代理內容可複用

     接下來要到最後一個版本了,版本二解決的問題是可以爲任何接口生成代理,那最後一個版本要解決的問題自然是可以爲任何接口生成任何代理的問題了,首先定義一個接口InvocationHandler,這麼起名字是因爲JDK提供的代理實例處理程序的接口也是InvocationHandler:

public interface InvocationHandler
{
    void invoke(Object proxy, Method method) throws Exception;
}

所以我們的Proxy類也要修改了,改爲:

public class ProxyVersion_2 implements Serializable
{
    private static final long serialVersionUID = 1L;
    
    public static Object newProxyInstance(Class<?> interfaces, InvocationHandler h) throws Exception
    {
        Method[] methods = interfaces.getMethods();        
        StringBuilder sb = new StringBuilder(1024);
        
        sb.append("package com.xrq.proxy;\n\n");
        sb.append("import java.lang.reflect.Method;\n\n");
        sb.append("public class $Proxy1 implements " +  interfaces.getSimpleName() + "\n");
        sb.append("{\n");
        sb.append("\tInvocationHandler h;\n\n");
        sb.append("\tpublic $Proxy1(InvocationHandler h)\n");
        sb.append("\t{\n");
        sb.append("\t\tthis.h = h;\n");
        sb.append("\t}\n\n");
        for (Method m : methods)
        {
            sb.append("\tpublic " + m.getReturnType() + " " + m.getName() + "()\n");
            sb.append("\t{\n");
            sb.append("\t\ttry\n");
            sb.append("\t\t{\n");
            sb.append("\t\t\tMethod md = " + interfaces.getName() + ".class.getMethod(\"" + m.getName() + "\");\n");
            sb.append("\t\t\th.invoke(this, md);\n");
            sb.append("\t\t}\n");
            sb.append("\t\tcatch (Exception e)\n");
            sb.append("\t\t{\n");
            sb.append("\t\t\te.printStackTrace();\n");
            sb.append("\t\t}\n");
            sb.append("\t}\n");
        }
        sb.append("}");
        
        /** 生成一段Java代碼 */
        String fileDir = System.getProperty("user.dir");
        String fileName = fileDir + "\\src\\com\\xrq\\proxy\\$Proxy1.java";
        File javaFile = new File(fileName);
        Writer writer = new FileWriter(javaFile);
        writer.write(sb.toString());
        writer.close();
        
        /** 動態編譯這段Java代碼,生成.class文件 */
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName);
        CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter);
        ct.call();
        sjfm.close();
        
        /** 將生成的.class文件載入內存,默認的ClassLoader只能載入CLASSPATH下的.class文件 */
        URL[] urls = new URL[] {(new URL("file:\\" + System.getProperty("user.dir") + "\\src"))};
        URLClassLoader ul = new URLClassLoader(urls);
        Class<?> c = Class.forName("com.xrq.proxy.$Proxy1", false, ul);
        
        /** 利用反射將c實例化出來 */
        Constructor<?> constructor = c.getConstructor(InvocationHandler.class);
        Object obj = constructor.newInstance(h);
        
        /** 使用完畢刪除生成的代理.java文件和.class文件,這樣就看不到動態生成的內容了 */
        File classFile = new File(fileDir + "\\src\\com\\xrq\\proxy\\$Proxy1.class");
        javaFile.delete();
        classFile.delete();
        
        return obj;
    }
}


     最明顯的變化,代理的名字變了,從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();
}
運行結果爲:

動態生成代理耗時:351ms
Before Hello World!
Hello World
After Hello World!

沒有問題
 
後記
     雖然我們自己寫了Proxy,但是JDK絕對不會用這種方式實現,原因無他,就是太慢。看到三個版本的代碼,運行時間都在300ms以上,效率如此低的實現,如何能給開發者使用?我拿JDK提供的Proxy和InvocationHandler自己寫了一個簡單的動態代理,耗時基本只在5ms左右。所以,文章的內容僅供學習、研究,知識點很多,如果能把這篇文章裏面的東西都弄懂,對於個人水平、對於Java很多知識點的理解,絕對是一個非常大的提高。


    見賢思齊,看到好的文章在此搬運過來沉澱傳播,自己寫一個java.lang.reflect.Proxy代理的實現  ,再次表示感謝!




            












發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章