1. 程式人生 > >論JDK動態代理的重要性及自定義自己的動態代理

論JDK動態代理的重要性及自定義自己的動態代理

1、JDK的動態代理

動態代理中的幾個角色:

1、被代理類

在jdk中被代理類必須實現介面

2、代理類

在jdk中代理類是執行時動態生成的,所以你在工程中看不到代理類

3、通知類也叫advice

在jdk中通知類必須實現invocationHandler介面

 

在這裡我們舉個例子幫助理解

比如,張三是一個IT狗,到了適婚年齡但是沒有物件,平時工作忙又沒有時間找物件,這時候張三的父母就著急了張羅著給張三介紹物件,這時候張三的父母就拿著張三的照片、身份證等等可以代表張三的資料去媒婆那。這個就是最典型的代理,張三的父母代張三找物件,而不是張三本人,其實張三本人就只需要結婚就行了。找物件和照顧孩子的事情就有張三的父母代理了。

 

從這個例子中,我們提取出幾個點:

1、張三,肯定是被代理物件,他要做的一件事情就是結婚。這裡我們抽象下,張三是一個人,所以他需要實現people介面

2、張三的父母,張三的父母是具體的執行者,比如,代理張三找物件,張三結婚後幫忙張三照顧孩子,所以我們這裡抽象出一個parent類,parent中有一個before方法,一個after方法。parent需要實現invocationHandler介面,它是一個通知類,負責具體執行

3、代理類,這是動態代理的難點,因為它是看不到摸不著的,它是存在在記憶體中的,且聽博主慢慢道來。

OK~~~看程式碼吧。

people介面:

public interface People {
    public void zhaoduixiang() throws Throwable;
}

 

張三類:

public class ZhangSan implements People {
    
    /* 
     * 張三這個人,平時加班很忙,
     * 他有生理問題,要解放雙手
     * 只關心有沒有一個幫他解放雙手這個人
     * 至於怎麼去找,他不關心
     */
    @Override
    public void zhaoduixiang() {
        System.out.println("=============================找到物件,解放雙手(你們懂的),然後結婚=====================");
    }
    
}

 

parent類:

/** 
 * @Description TODO 
 * @ClassName   Parent 
 * @Date        2018年9月26日 下午8:12:35 
 * @Author      zg-jack
 * 
 * 這個就是張三的父母類
 * 這個類的目的就是為了增強張三的zhaoduixiang()方法
 * 
 * 
 * 張三的父母需要持有張三這個人的引用
 * 
 * 
 */

public class Parent implements InvocationHandler {
    
    private People people;
    
    public Parent(People people) {
        this.people = people;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //前置增強
        before();
        
        //2、呼叫張三的這個人的zhaoduixiang方法
        method.invoke(people, args);
        
        //3、呼叫after()
        after();
        return null;
    }
    
    /** 
     * @Description TODO 
     * @param  引數 
     * @return void 返回型別  
     * @throws 
     * 
     * 在張三找到物件之前,幫助張三找物件
     */
    
    public void before() {
        System.out.println("&&&&&&&&&&&&&&&張三的父母,幫助張三找物件&&&&&&&&&&&&&&&&");
    }
    
    /** 
     * @Description TODO 
     * @param  引數 
     * @return void 返回型別  
     * @throws 
     * 
     * 找到物件要以後,張三父母幫他操持結婚
     */
    
    public void after() {
        System.out.println("&&&&&&&&&&&&&&&張三的父母,幫助張三操持結婚的事情&&&&&&&&&&&&&&&&");
    }
    
}

測試類:

public class MyTest {
    
    public static void main(String[] args) throws Throwable {
        //這個返回的物件就是張三這個人的代理物件
        People proxyPeople = (People)Proxy.newProxyInstance(MyTest.class.getClassLoader(),
                new Class<?>[] {People.class},
                new Parent(new ZhangSan()));
        createProxyClassFile();
        
        proxyPeople.zhaoduixiang();
    }
    
    /** 
     * @Description TODO 
     * @param  引數 
     * @return void 返回型別  
     * @throws 
     * 
     * 用流的方式把記憶體中的代理物件的位元組碼輸出到.class檔案中
     */
    
    public static void createProxyClassFile() {
        byte[] data = ProxyGenerator.generateProxyClass("$Proxy0",
                new Class[] {People.class});
        
        try {
            FileOutputStream out = new FileOutputStream("$Proxy0.class");
            out.write(data);
            out.close();
        }
        catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
}

測試結果:

&&&&&&&&&&&&&&&張三的父母,幫助張三找物件&&&&&&&&&&&&&&&&
=============================找到物件,解放雙手(你們懂的),然後結婚=====================
&&&&&&&&&&&&&&&張三的父母,幫助張三操持結婚的事情&&&&&&&&&&&&&&&&

從這個測試結果,我們僅僅用了一行呼叫程式碼

proxyPeople.zhaoduixiang();就出現了這個結果,而這個結果明顯又是parent類中的invoke方法的列印結果,那麼這裡問題來了,誰呼叫了parent中的invoke方法???OK,帶著這個問題,我們看一下圖,從圖中瞭解動態代理的呼叫過程,回答前面的代理類問題,如圖:

從圖中我們可以看出,proxypeople.zhaoduixiang()呼叫的實際上是記憶體中的代理物件,這個物件是程式在執行時的時候動態載入到記憶體裡的,所以程式設計師看不到,那麼有什麼辦法可以看到這個記憶體中的代理物件的程式碼呢,有辦法~~~這個博主待會兒講。從圖中我們可以看出,帶代理物件中,有一個屬性,invocationHandler h屬性,而h屬性的值就是parent物件,而在代理物件中的方法zhaoduixiang中就只有一行代理,h.invoke(.......),其實就是掉到了parent物件中的invoke方法。。。這裡就回答了剛剛那個問題,誰調了parent物件的invoke方法? 這下我們知道了,是記憶體中的代理物件調到了invoke方法。。。

那麼還是回到前面的問題,記憶體中的代理物件,我們能拿到嗎??OK,貼程式碼

/** 
     * @Description TODO 
     * @param  引數 
     * @return void 返回型別  
     * @throws 
     * 
     * 用流的方式把記憶體中的代理物件的位元組碼輸出到.class檔案中
     */
    
    public static void createProxyClassFile() {
        byte[] data = ProxyGenerator.generateProxyClass("$Proxy0",
                new Class[] {People.class});
        
        try {
            FileOutputStream out = new FileOutputStream("$Proxy0.class");
            out.write(data);
            out.close();
        }
        catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    

我們可以拿到位元組碼,然後用流的方式輸出到.class檔案中,然後用反編譯工具反編譯一下就可以了。。

 

為了讓大家徹底領悟動態代理技術,我打算自己定義一個動態代理,不用jdk的proxy類和invocationHandler介面,這樣才能徹底領悟

貼程式碼吧:

自定義的JackInvocationHandler介面,類似於JDK中的InvocationHandler介面

public interface JackInvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
}

最關鍵的是獲取代理物件,因為這個代理物件在記憶體裡面,在JDK中我們用Proxy這個類獲取代理物件

People proxyPeople = (People)Proxy.newProxyInstance(MyTest.class.getClassLoader(),
                new Class<?>[] {People.class},
                new Parent(new ZhangSan()));

那麼我們自己定義的動態代理裡面,這個代理物件是如何獲取呢??

MyParent類,類似於parent類

public class MyParent implements JackInvocationHandler {
    
    private People people;
    
    public MyParent(People people) {
        this.people = people;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //前置增強
        before();
        
        //2、呼叫張三的這個人的zhaoduixiang方法
        method.invoke(people, args);
        
        //3、呼叫after()
        after();
        return null;
    }
    
    /** 
     * @Description TODO 
     * @param  引數 
     * @return void 返回型別  
     * @throws 
     * 
     * 在張三找到物件之前,幫助張三找物件
     */
    
    public void before() {
        System.out.println("&&&&&&&&&&&&&&&張三的父母,幫助張三找物件&&&&&&&&&&&&&&&&");
    }
    
    /** 
     * @Description TODO 
     * @param  引數 
     * @return void 返回型別  
     * @throws 
     * 
     * 找到物件要以後,張三父母幫他操持結婚
     */
    
    public void after() {
        System.out.println("&&&&&&&&&&&&&&&張三的父母,幫助張三操持結婚的事情&&&&&&&&&&&&&&&&");
    }
    
}

MyProxy類,這個是最關鍵的核心程式碼,其功能類似於JDK中的Proxy類

public class MyProxy {
    
    private static String rt = "\r\n";
    
    /** 
     * @Description TODO 
     * @param @param loader
     * @param @param interfaces
     * @param @param h
     * @param @return 引數 
     * @return Object 返回型別  
     * @throws
     * 
     *  這個方法最終是需要返回一個代理物件
     *  1、執行時動態生成
     *  2、它是一個類
     *  3、它要實現people介面
     *  4、是以位元組碼的方式載入到記憶體中
     *    
     */
    public static Object newProxyInstance(ClassLoader loader,
            Class<?>[] interfaces, JackInvocationHandler h) {
        
        String fileName = "F:/workspace/proxy/src/main/java/com/zhuguang/jack/myproxy/$Proxy0.java";
        
        //1、我們要生成一個類,以字串拼湊的方式,拼湊出一個類
        String javaClassStr = getJavaStr(interfaces);
        
        //2、通過流的方式生成java檔案
        createJavaFile(javaClassStr, fileName);
        
        //3、動態編譯java檔案,生成.class檔案
        compilerJava(fileName);
        
        //4、要把磁盤裡面的.class檔案內容載入到jvm的記憶體
        Object instance = LoadClass(h);
        return instance;
    }
    
    /** 
     * @Description TODO 
     * @param @param h
     * @param @return 引數 
     * @return Object 返回型別  
     * @throws 
     * 
     * 自定義一個類載入器
     */
    
    private static Object LoadClass(JackInvocationHandler h) {
        JackClassLoader jackClassLoader = new JackClassLoader(
                "F:/workspace/proxy/src/main/java/com/zhuguang/jack/myproxy");
        try {
            //返回的是一個被代理物件的Class物件
            Class<?> findClass = jackClassLoader.findClass("$Proxy0");
            Constructor<?> constructor = findClass.getConstructor(JackInvocationHandler.class);
            Object newInstance = constructor.newInstance(h);
            return newInstance;
        }
        catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
    
    private static void compilerJava(String fileName) {
        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
        
        StandardJavaFileManager standardFileManager = systemJavaCompiler.getStandardFileManager(null,
                null,
                null);
        
        Iterable<? extends JavaFileObject> javaFileObjects = standardFileManager.getJavaFileObjects(fileName);
        
        CompilationTask task = systemJavaCompiler.getTask(null,
                standardFileManager,
                null,
                null,
                null,
                javaFileObjects);
        task.call();
        try {
            standardFileManager.close();
        }
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    private static void createJavaFile(String javaClassStr, String fileName) {
        
        try {
            File f = new File(fileName);
            FileWriter fw;
            fw = new FileWriter(f);
            fw.write(javaClassStr);
            fw.flush();
            fw.close();
        }
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
    }
    
    private static String getJavaStr(Class<?>[] interfaces) {
        
        Method[] methods = interfaces[0].getMethods();
        
        String proxyClassStr = "package com.zhuguang.jack.myproxy;" + rt
                + "import java.lang.reflect.Method;" + rt
                + "public class $Proxy0 implements " + interfaces[0].getName()
                + "{" + rt + "JackInvocationHandler h;" + rt
                + "public $Proxy0(JackInvocationHandler h) {" + rt
                + "this.h=h;" + rt + "}"
                + getMethodString(methods, interfaces[0]) + rt + "}";
        
        return proxyClassStr;
    }
    
    private static String getMethodString(Method[] methods, Class intf) {
        
        String proxyMe = "";
        
        for (Method method : methods) {
            proxyMe += "public void " + method.getName()
                    + "() throws Throwable {" + rt + "Method md = "
                    + intf.getName() + ".class.getMethod(\"" + method.getName()
                    + "\",new Class[]{});" + rt
                    + "this.h.invoke(this,md,null);" + rt + "}" + rt;
            
        }
        
        return proxyMe;
    }
}

在這個類中有幾個步驟:

1、生成代理類,我們用字串拼湊的方式把類中的內容拼湊成String型別

2、通過流的方式把String寫到java檔案中

3、動態編譯java檔案,把它編譯成.class檔案

4、把磁碟中的.class檔案載入到記憶體中並返回代理類的物件

通過這幾個步驟,我們就可以拿到記憶體中的代理類,如此一個自己定義的動態代理就搞定了,是不是沒有那麼難?

 

希望博主的整理能夠幫助到大家,如果大家覺得OK的話,麻煩點個贊,或者寫個評論什麼的,好歹我也要士氣來 繼續堅持我的原創部落格之路啊,哈哈~~~~~