1. 程式人生 > >基於JDK的動態代理技術詳解

基於JDK的動態代理技術詳解

end course log 些許 private provide url 模仿 ade

雖然對於Spring的基本思想Aop是基於動態代理和CGlib這一點很早就有所認識,但是什麽是動態代理卻不甚清楚。為了對Spring加深理解,我覺得好好學習一下java的動態代理是非常有必要的。

靜態代理

在學習動態代理之前我先花一點時間了解一下靜態代理,從靜態代理出發了解代理到底是怎麽一回事,以及了解靜態代理的局限性,進而明白為什麽要發展及使用動態代理技術。

相信使用過Spring框架的同學都知道Spring利用Aop完成聲明式事務管理以及其他的代理增強,也就是在方法執行前後加上一些譬如時間、日誌、權限控制等。假如現在我們從比較復雜的Spring Aop中跳出來,那麽,有什麽簡單的方法能夠對我們的方法進行增強呢?

繼承實現

最簡單的方法就是繼承,子類繼承父類並重寫父類的方法,在重寫的過程總就可以對原有的方法進行增強。下面代碼就是這種代理增強思想的實現。

package cn.proxy;

public class Tank implements Moveable{

    @Override
    public void move() {
        System.out.println("tank moveing...");        
    }

}

// son
package cn.proxy;

public class LogProxyTank extends
Tank { public void move() { System.out.println("tank start..."); super.move(); System.out.println("tank stop..."); } }

可以看見,通過重寫父類方法可以非常方便實現代理增強,但是就一個日誌功能的代理增強,假如涉及到100個類呢?那就要為100個類實現子類。這還不算麻煩,假如涉及到日誌、時間、權限等多個功能的增強的時候,先時間後日誌和先日誌後時間可不是一回事,那麽就要分別實現兩個代理的子類。想一想這其中涉及到許多增強的功能和許多被代理類時,就會造成代理類爆炸。顯然這種方式是很不靈活的。

聚合實現

被代理類和代理類實現同一個接口,同時代理類不再與被代理類存在繼承關系,而是代理類包含一個被代理類類型的成員變量。

package cn.proxy;

public class LogProxy implements Moveable {
    
    private Moveable m;
    
    public LogProxy(Moveable m) {
        super();
        this.m = m;
    }

    @Override
    public void move() throws Exception {
        System.out.println("moveable start...");
        m.move();
        System.out.println("moveable stop...");
    }
    
}

可以看見,相較於繼承方式,聚合的方式實現代理增強,通過傳入不同的被代理類,可以實現對不同的被代理進行增強,但是這種方式實現不同的增強還是需要寫不同的代理類,靈活性上還不是很完美。

動態代理

我們都知道,一個類要經過編寫、編譯、加載進JVM最終才能進行實例化。通常這些工作是分開進行的。那麽有沒有可能將這些過程封裝到一個方法裏面呢?

答案是可以的。現在大致講一下步驟:

1. 首先將可以將源碼保存成字符串,並將增強的代碼加進去,然後通過Java IO技術將其寫入到一個java文件中

2. 使用JavaCompiler進行編譯生成.class的二進制文件

3. 使用一個類加載器(這裏使用URLClassLoader)將二進制文件加載進內存

4. 實例化代理對象

基本思路就是這樣,現在要實現靈活的java動態代理,問題就是如何動態的確定對什麽類進行代理,進行怎樣的代理增強。關鍵就在步驟1,源碼字符串不能寫死,而應該動態生成。那麽這個方法的參數就需要有被代理對象和增強的邏輯。

被代理類很容易理解,就是要通過這個被代理類知道要對哪些方法進行代理增強,通過反射就可以獲取被代理類非所有方法。還有一個重要的接口是InvocationHandler,這個接口有一個方法invoke,在這個方法中可以定義代理邏輯以及調用被代理類的實例對象的欲代理方法來完成主要的方法邏輯,因為代理類是不能完成被代理類的方法邏輯的。就像歌星(被代理類)的經紀人(代理類)可以幫助歌星去接演出、安排行程,但是不能代替歌星去唱歌一樣。

為了進一步了解Java動態代理,可以對JDK中的Proxy類進行一個簡單的模擬,代碼如下:

package cn.proxy;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class Proxy {
    
    public static Object newProxyInstance(Class interfacee, InvocationHandler handler) throws Exception {
        
        String rt = "\r\n";
        // 源碼
        String methodStr = "";
        Method[] methods = interfacee.getMethods();
        for (Method m : methods) {
            methodStr +=
                    rt +
            "    @Override" + rt +        
            "    public void " + m.getName() + "() throws Exception {" + rt +
            "        Method md = " + interfacee.getName()+ ".class.getMethod(\"" + m.getName() + "\");" + rt +
            "        handler.invoke(this, md);" + rt +
            "    }" + rt;
        }
        
        String src = 
        "package cn.proxy;" + rt + rt +
        
        "import java.lang.reflect.Method;" + rt +
        "import cn.proxy.InvocationHandler;" + rt + rt +

        "public class TankTimeProxy implements " +  interfacee.getName() + "{" + rt +
            
        "    private InvocationHandler handler;" + rt + rt +
            
        "    public TankTimeProxy(InvocationHandler handler) {" + rt +
        "        super();" + rt +
        "        this.handler = handler;" + rt +
        "    }" + rt +
        
        methodStr +

        "}";
        
        // 生成一個java源碼
        String fileName = System.getProperty("user.dir") + "/src/cn/proxy/TankTimeProxy.java";
        File file = new File(fileName);
        FileWriter fileWriter = new FileWriter(file);
        fileWriter.write(src);
        fileWriter.flush();
        fileWriter.close();
        
        // 使用jdk編譯api動態編譯
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(fileName);
        CompilationTask task = compiler.getTask(null, fileManager, null, null, null, units);
        task.call();
        fileManager.close();
        
        // 將二進制文件加載進入內存
        URL[] urls = new URL[] {new URL("file:/" + System.getProperty("user.dir") + "src")};
        URLClassLoader cl = new URLClassLoader(urls);
        Class<?> clazz = cl.loadClass("cn.proxy.TankTimeProxy");
        System.out.println(clazz);
        
        // 實例化新對象
        Constructor<?> constructor = clazz.getConstructor(InvocationHandler.class);
        Object m = constructor.newInstance(handler);
        
        return m;
    }
}

仔細分析一下上面的代碼可以看出,基本思路就是上面所述的四步走。通過反射技術來動態解析傳入的被代理類,獲取欲代理的方法,而每個方法的具體實現交由InvocationHandler的具體實現類來完成,其invoke方法完成代理增強並調用被代理類的相應的被代理方法。

為了對InvocationHandler有一個直觀的了解,寫一個簡單的InvocationHandler接口以及實現類,具體代碼如下:

package cn.proxy;

import java.lang.reflect.Method;

public interface InvocationHandler {
    
    void invoke(Object o, Method m);

}
package cn.proxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TimeHandler implements InvocationHandler {
    
    // 被代理類
    private Tank t;
    
    public TimeHandler(Tank t) {
        this.t = t;
    }

    @Override
    public void invoke(Object o, Method m) {
        long start = System.currentTimeMillis();
        System.out.println("start time: " + start);
        try {
            m.invoke(t, new Object[]{});
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("end time: " + end);
    }

}

InvocationHandler的實現類中有一個要註意:要有一個被代理類類型的成員變量,要通過這個變量才能過調用被代理類的相應方法來完成主要的方法功能。通過 Proxy.newProxyInstance 創建的代理對象是在jvm運行時動態生成的一個對象,它並不是InvocationHandler類型,也不是定義的那接口的類型,而是在運行是動態生成的一個對象。

總結

簡單理解,代理模式就是在一個方法執行前後加入代理增強的代碼。我們不斷探索新的方式,就是要實現更好的靈活性,可以對任意的被代理類實現任意的代理增強。動態代理技術通過傳入被代理類(這就可以任意傳入),反射解析這個類來獲得欲代理的方法。通過傳入InvocationHandler的實現類來實現方法的代理增強,可以根據傳入的實現類的不同來實現任意的代理,而且這個實現類可以做到復用,就像機械零件一樣,自由組裝實現不同形式的代理組合。Java具體的代理類Proxy與本文中模仿的代理類有些許不同,包括可以傳入方法參數實現對有參方法的代理,代理類被命名為$proxy1等等。但這些是細節問題,主要的思想還是一樣的。

===========================================================================================================================

本文只是我現階段的學習心得總結而成,內容可能不夠深入,由於水平所限,不保證所有內容正確,歡迎有同學在評論中指正,萬分感謝!

保證每一個字的原創性!

我是一個程序員,我所能做的就是每一天都在進步,面對技術保持一顆赤子之心,這是我人生現階段全部的追求。"Stay hungry, stay foolish"!

===========================================================================================================================

基於JDK的動態代理技術詳解