從0到1用故事講解「動態代理」

蟬沐風發表於2022-01-27

雖然學會了靜態代理,但是招財這幾天仍然是有些悶悶不樂,因為始終沒有想出上次陀螺留給自己的問題的解決思路。 ​

如何為任意物件任意方法前後新增同一個處理邏輯?

手動為每一個物件的每一個方法中新增同一段程式碼邏輯是不可能的,這輩子都不可能的。「懶」是科技進步的重要動力! ​

思考未果,招財終於要求助陀螺了。

捉襟見肘的靜態代理

“師傅,你上次留給我的問題我沒想通。這種需求的現實意義在哪兒呢?”招財開門見山。 ​

陀螺說:“如果真的能在任意方法前後新增自己的邏輯,那作用可就太大了!你可以在邏輯執行之前先校驗操作許可權;你也可以在邏輯執行之前先開始一個事務,在邏輯完成之後提交或回滾事務。這種功能怎麼用完全取決於你的想象力。” ​

“真沒想到居然有這麼大的作用!那麼該怎麼實現呢?” ​

“你覺得靜態代理能不能解決這個問題?”陀螺反問道。 ​

招財回答說:“可以倒是可以,我們可以為每個類針對每一種邏輯編寫一個靜態代理,但是問題就在這,如果被代理的類很多,代理邏輯也很多,就會造成類爆炸的局面啊。” ​

“我覺得靜態代理更適合為某些特定的介面實現代理,而且代理物件必須顯式地建立。”招財繼續補充道。 ​

陀螺:“你說的沒錯,問題就在於靜態代理需要顯式地建立代理物件,那如果我們能夠動態生成代理物件,而這個生成過程使用者完全無感知,這個問題是不是就可以解決了呢?” ​

“真的有這種方法嗎?”招財的眼睛裡都發著光。 ​

“這就是動態代理了。這件事情確實很難,我們需要一點點地來完成這件事情,跟上我的思路,保證能讓你徹底理解動態代理!”陀螺自信地對招財說。 ​

動態代理的誕生

“首先回憶一下靜態代理中你編寫的日誌代理。”說著,陀螺給出了程式碼。 ​

//程式碼1-1
package designPattern.proxy.dynamicProxy.v1;
​
import designPattern.proxy.dynamicProxy.Payable;
​
public class SiShiDaDaoLogProxy implements Payable {
​
    //被代理物件
    private Payable payable;
​
    public SiShiDaDaoLogProxy(Payable payable) {
        this.payable = payable;
    }
​
​
    @Override
    public void pay() {
        System.out.println("列印日誌1");
​
        payable.pay();
​
        System.out.println("列印日誌2");
    }
}

“這個程式碼你應該已經非常熟悉了吧。”陀螺問招財。 ​

“是啊,payable是被代理物件,SiShiDaDaoLogProxy是生成的代理類,代理物件實現了Payable介面,在重寫pay()方法的時候進行了邏輯增強,但是本質上仍然呼叫的是被代理物件的方法。”招財回答得很流利。 ​

陀螺點了點頭,“很好,假設現在我們通過某種方式獲得了上面的原始碼,現在我們的目標是要動態生成這個代理物件。” ​

“動態生成?我只知道一開始學習Java的時候,通常會先寫一個HelloWorld.java原始檔,然後利用javac工具編譯成HelloWorld.class檔案,你說的動態生成和這個有關係嗎?”招財問道。 ​

“原理是類似的,我們需要把上面的SiShiDaDaoLogProxy寫入到磁碟中生成.java檔案,然後利用JDK提供的編譯工具轉為.class檔案,再通過類載入器將.class檔案載入到JVM中,最後我們通過反射就能獲得SiShiDaDaoLogProxy例項物件了。” ​

“這......這涉及到的知識點也太多了!JDK提供的編譯工具我甚至都沒有聽過,類載入的知識也幾乎已經忘光了,也就反射總在框架中遇到,多少還有點印象。師傅,我是不是得先補一補這些知識點啊?”招財有點絕望地問。

"你這種想法是很多初學者的通病,學一個知識點的時候總是不自覺地把其他相關知識點也學了一遍,最後忘了自己一開始的學習目的是什麼,本末倒置。記住,要先掌握脈絡,再學細節!"陀螺正色道。 ​

陀螺看著招財還是有點不自信,繼續說道:“別擔心,要不是碰到這個動態代理,JDK自帶的編譯器恐怕你這輩子也用不上了,所以你只要知道它的作用是什麼即可,程式碼都不需要看懂。至於類載入機制,你要理解我們需要一個類載入器來載入上一步得到的.class檔案到JVM虛擬機器中,這樣才能生成例項物件,瞭解這些就夠了。至於反射,你確實應該掌握,好在它本身非常簡單,跟著我的思路就能理解了。” ​

陀螺的話讓招財安心了許多,重新打起了精神。 ​

v1.0——先動態編譯一段原始碼吧

“我們先建立一個類Proxy,在裡面定義一個newProxyInstance的靜態方法,該方法返回一個Object物件,這個物件就是我們最終生成的代理物件。”說罷,陀螺給出了程式碼。 ​

/**
 * @author chanmufeng
 * @description 動態代理v1
 * @date 2022/1/10
 */
public class Proxy {
​
    //定義換行符
    private static final String ln = "\r\n";
​
    public static Object newProxyInstance(ClassLoader classLoader) {
        try {
            
            /** 1.生成原始碼 **/
            String src = generateSrc();
​
            /** 2.將原始碼寫入磁碟,生成.java檔案 **/
            File file = createJavaFile(src);
​
            /** 3.將生成的.java檔案編譯成.class檔案 **/
            compile(file);
​
            /** 4.類載入器將.class檔案載入到JVM **/
            Class proxyClass = classLoader.loadClass("SiShiDaDaoLogProxy");
            
            /** 5.利用反射例項化物件 **/
            Constructor proxyConstructor = proxyClass.getConstructor(Payable.class);
            file.delete();
            Payable p = (Payable) proxyConstructor.newInstance(new SiShiDaDao());
            
            return p;
​
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
​
    }
​
}

“為了方便你理解,我把每個步驟的程式碼分別作了封裝,步驟2和步驟3你只需要理解他們的含義就行了,具體的程式碼不是研究的重點。這兩個步驟的程式碼在接下來的講述中幾乎不會發生變化,因此接來的講述我會用createJavaFilecompile來分別代替兩個步驟,不會再給出具體程式碼。”陀螺對招財解釋道。 ​

“如此一來,客戶只需要呼叫Proxy.newProxyInstance(ClassLoader classLoader)就能得到SiShiDaDaoLogProxy物件例項了是吧。”招財問。 ​

“沒錯。” ​

“可是,我看到newProxyInstance方法有個引數,需要傳一個ClassLoader,這個引數是什麼意思?”招財有點不解地問。 ​

“還記得我們需要一個類載入器來載入步驟3生成的.class檔案到JVM中嗎?這個引數就是類載入器的一個例項,提供這個引數是讓客戶可以靈活地選擇不同的類載入器來完成這個操作。” ​

招財撅了噘嘴,“我不理解這個引數提供的必要性,你直接預設一個類載入器不是更好嗎?我覺得大部分的使用者都不知道這個引數該傳什麼值吧。” ​

“別急,之後你就會知道我設計這個引數的意圖了。為了讓你知道怎麼傳這個引數,我自定義了一個類載入器,這個操作其實並不難。” ​

“還有第5步,我也不是很懂。”招財繼續追問。 ​

“別急,先看一下我們目前為止的所有程式碼,然後解釋給你聽。”

package designPattern.proxy.dynamicProxy;
​
/**
 * @author 蟬沐風
 * @description 支付介面
 * @date 2022/1/10
 */
public interface Payable {
​
    /**
     * 支付介面
     */
    void pay();
}
package designPattern.proxy.dynamicProxy;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @author 蟬沐風
 * @description 「四十大盜」金融公司提供的第三方介面,實現了支付介面
 * @date 2022/1/10
 */
public class SiShiDaDao implements Payable {
​
    @Override
    public void pay() {
        try {
            // ...
            System.out.println("「四十大盜」支付介面呼叫中......");
            //模擬方法呼叫延時
            TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 6000));
            // ...
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
 * @author 蟬沐風
 * @description 動態代理v1
 * @date 2022/1/10
 */
public class Proxy {
​
    //定義換行符
    private static final String ln = "\r\n";
​
    public static Object newProxyInstance(ClassLoader classLoader) {
        try {
​
            /** 1.生成原始碼 **/
            String src = generateSrc();
​
            /** 2.將原始碼寫入磁碟,生成.java檔案 **/
            File file = createJavaFile(src);
​
            /** 3.將生成的.java檔案編譯成.class檔案 **/
            compile(file);
​
            /** 4.類載入器將.class檔案載入到JVM **/
            Class proxyClass = classLoader.loadClass("SiShiDaDaoLogProxy");
            
            /** 5.利用反射例項化物件 **/
            Constructor proxyConstructor = proxyClass.getConstructor(Payable.class);
            file.delete();
            Payable p = (Payable) proxyConstructor.newInstance(new SiShiDaDao());
            return p;
​
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        return null;
​
    }
​
    private static String generateSrc() {
        StringBuilder sb = new StringBuilder();
        sb.append("package designPattern.proxy.dynamicProxy.v1;").append(ln)
                .append("import designPattern.proxy.dynamicProxy.Payable;").append(ln)
                .append("public class SiShiDaDaoLogProxy implements Payable { ").append(ln)
                .append("    private Payable payable;").append(ln)
                .append("    public SiShiDaDaoLogProxy(Payable payable) {").append(ln)
                .append("        this.payable = payable;").append(ln)
                .append("    }").append(ln)
                .append("    @Override").append(ln)
                .append("    public void pay() {").append(ln)
                .append("        System.out.println("列印日誌1");").append(ln)
                .append("        payable.pay();").append(ln)
                .append("        System.out.println("列印日誌2");").append(ln)
                .append("    }").append(ln)
                .append("}");
        return sb.toString();
    }
​
    private static File createJavaFile(String src) throws Exception {
        String filePath = Proxy.class.getResource("").getPath();
        File file = new File(filePath + "SiShiDaDaoLogProxy.java");
        FileWriter fw = new FileWriter(file);
        fw.write(src);
        fw.flush();
        fw.close();
        return file;
    }
​
    private static void compile(File file) throws IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
        Iterable iterable = manager.getJavaFileObjects(file);
        JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, iterable);
        task.call();
        manager.close();
    }
}
/**
 * @author 蟬沐風
 * @description 自定義類載入器
 * @date 2022/1/10
 */
public class MyClassLoader extends ClassLoader {
​
    private File classPathFile;
​
    public MyClassLoader() {
        String classPath = MyClassLoader.class.getResource("").getPath();
        this.classPathFile = new File(classPath);
    }
​
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
​
        String className = MyClassLoader.class.getPackage().getName() + "." + name;
        if (classPathFile != null) {
            File classFile = new File(classPathFile, name.replaceAll("\.", "/") + ".class");
            if (classFile.exists()) {
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try {
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte[] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1) {
                        out.write(buff, 0, len);
                    }
                    return defineClass(className, out.toByteArray(), 0, out.size());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

“我再囉嗦一遍目前為止我們做的事情。”陀螺耐心地解釋。 ​

  1. 我們通過generateSrc方法得到了SiShiDaDaoLogProxy類的原始碼,這個原始碼就是一開始給你看的靜態代理的程式碼(重點)
  1. 將原始碼檔案寫入磁碟,生成SiShiDaDaoLogProxy.java檔案(不是重點)
  1. 利用JDK提供的編譯工具,將SiShiDaDaoLogProxy.java編譯成SiShiDaDaoLogProxy.class檔案(不是重點)
  1. 使用自定義的類載入器(MyClassLoader)將SiShiDaDaoLogProxy.class載入到記憶體(不是重點)
  1. 使用反射,得到SiShiDaDaoLogProxy的例項物件(重點)

“你會發現,generateSrc生成的原始碼只有一個有參的建構函式,因此第5步需要通過反射獲取這個有參的建構函式物件,並傳入new SiShiDaDao()進行例項化,效果和下面的程式碼是一樣的。” ​

new SiShiDaDaoLogProxy(new SiShiDaDao());

“這個我懂了,”招財點點頭,“但是你說在第2步和第3步分別生成了兩個檔案,這兩個檔案儲存在哪裡了呢?” ​

“這就是動態代理的奇妙之處了!它自動給你生成了原始碼檔案和位元組碼檔案,對此你卻毫無感知。你甚至都不需要知道自動生成的類的名字是什麼。這裡我也不會告訴你檔案儲存在哪裡了,因為這個問題並不重要,之後你自己執行程式碼看看就知道了。”陀螺解釋說,“我們現在執行一下客戶端程式,看看有什麼結果吧。”

/**
 * @author 蟬沐風
 * @description 呼叫客戶端
 * @date 2022/1/10
 */
public class Client {
    public static void main(String[] args) {
        Payable payable = (Payable) Proxy.newProxyInstance(new MyClassLoader());
        payable.pay();
    }
}

執行結果如下 image.png 看見陀螺興奮的樣子,招財有點為難,因為她不明白折騰了這麼久,最終得到的竟是和之前靜態代理一樣的執行效果。 ​

吾愛吾師,吾更愛真理! ​

招財鼓起勇氣,問道:“這個結果和靜態代理的執行結果沒有差別,不是嗎?” ​

陀螺從招財委婉的話裡聽出了她的困惑,“結果雖然一樣,但是實現機制卻發生了翻天覆地的變化。你有沒有發現,我們沒有手寫任何的代理類。之前靜態代理還需要手寫SiShiDaDaoLogProxy,我們完全是自動生成的。” ​

“你說的這一點我理解了。但是目前自動生成的都是寫死的程式碼,也就是說目前只能為SiShiDaDao這個類中的pay()方法做代理,效果還差得遠呢。” ​

“你說得沒錯,接下來我們就稍微改進一下,這個階段我們的目標是,要得到一個物件,這個物件可以代理實現了任意介面的類,從而被代理類中的每一個方法前後都會新增我們的日誌邏輯。”

v2.0——為實現了任意介面的類做日誌代理

陀螺問招財,“如果你是設計者,站在使用者的角度讓你來設計這個介面,你會怎麼設計?” ​

招財思考了一番,“newProxyInstance方法裡應該新增另一個引數,用來指代被代理物件實現的介面,意思就是我要得到實現了這個介面的類的代理物件。” ​

/**
 * @author chanmufeng
 * @description 動態代理v2
 * @date 2022/1/10
 */
public class Proxy {
​
  ...
        
    public static Object newProxyInstance(ClassLoader classLoader, Class intfce) {
        
        ...
        
    }
​
}

“很好。這樣一來,我們就不能在generateSrc方法中將生成的類的實現關係寫死,需要一點變化。看下圖,所有用紅色線框圈出來的部分都是需要動態修改的,而且更麻煩的一點是,我們還需要動態生成這個介面中宣告的所有的方法,包括方法的引數和返回值資訊。” ​

image.png

“我想,這一定又離不開反射吧。”招財無奈地說道。 ​

“是的,重點體會思想。別擔心,這些程式碼很容易理解,但是需要你多看幾遍。接下來我們來實現新的generateSrc方法。”陀螺繼續說道,“但是下面的程式碼可能會讓你有點不適,因為通過拼接字串的方式獲取原始碼,可讀性很差。但是先體會思想,之後我會讓你看到最終動態生成的原始碼內容,你也就明白了下面的程式碼究竟做了什麼。” ​

private static String generateSrc(Class intfce) {
​
        //獲取介面所在包名
        String packageName = intfce.getPackage().getName() + "." + intfce.getSimpleName();
​
        StringBuilder sb = new StringBuilder();
        sb.append("package designPattern.proxy.dynamicProxy.v2;").append(ln)
                .append("import ").append(packageName).append(";").append(ln)
                .append("public class $Proxy0 implements ").append(intfce.getName()).append(" { ").append(ln)
                .append("    private ").append(intfce.getSimpleName()).append(" obj;").append(ln)
                .append("    public $Proxy0(").append(intfce.getSimpleName()).append(" obj) {").append(ln)
                .append("        this.obj = obj;").append(ln)
                .append("    }").append(ln).append(ln)
​
                .append(generateMethodsSrc(intfce))
​
                .append("}").append(ln).append(ln);
​
        System.out.println(sb.toString());
        return sb.toString();
    }
​
    private static StringBuilder generateMethodsSrc(Class intfce) {
        StringBuilder sb = new StringBuilder();
​
        for (Method m : intfce.getMethods()) {
            sb.append("    @Override").append(ln);
​
            Class<?>[] params = m.getParameterTypes();
            StringBuilder paramNames = new StringBuilder();
            StringBuilder paramValues = new StringBuilder();
            StringBuilder paramClasses = new StringBuilder();
​
            for (int i = 0; i < params.length; i++) {
                Class clazz = params[i];
                String type = clazz.getName();
                String paramName = toLowerFirstCase(clazz.getSimpleName()) + i;
                paramNames.append(type + " " + paramName);
                paramValues.append(paramName);
                paramClasses.append(clazz.getName() + ".class");
                if (i < params.length - 1) {
                    paramNames.append(",");
                    paramValues.append(",");
                    paramClasses.append(",");
                }
            }
​
            sb.append("    public ").append(m.getReturnType().getName()).append(" ").append(m.getName())
                    .append("(").append(paramNames).append("){").append(ln);
​
            sb.append("        System.out.println("列印日誌1");").append(ln)
                    .append("        obj.").append(m.getName()).append("(").append(paramValues).append(");").append(ln)
                    .append("        System.out.println("列印日誌2");").append(ln)
                    .append("    }").append(ln).append(ln);
​
        }
​
        return sb;
    }
​
    private static String toLowerFirstCase(String src) {
        char[] chars = src.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
public static Object newProxyInstance(ClassLoader classLoader, Class intfce) {
        try {
​
            /** 1.生成原始碼 **/
            String src = generateSrc(intfce);
​
            /** 2.將原始碼寫入磁碟,生成.java檔案 **/
            File file = createJavaFile(src);
​
            /** 3.將生成的.java檔案編譯成.class檔案 **/
            compile(file);
​
            /** 4.類載入器將.class檔案載入到JVM **/
            Class proxyClass = classLoader.loadClass("$Proxy0");
            Constructor proxyConstructor = proxyClass.getConstructor(intfce);
            file.delete();
            Payable p = (Payable) proxyConstructor.newInstance(new SiShiDaDao());
            return p;
​
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        return null;
​
    }

此時客戶端呼叫 ​

public class Client {
    public static void main(String[] args) {
        Payable payable = (Payable) Proxy.newProxyInstance(new MyClassLoader(), Payable.class);
        payable.pay();
    }
}

執行結果如下 ​

image.png 動態生成的程式碼如下 ​

注:程式碼為動態生成的原始內容,未經IDE格式化

package designPattern.proxy.dynamicProxy.v2;
import designPattern.proxy.dynamicProxy.Payable;
public class $Proxy0 implements designPattern.proxy.dynamicProxy.Payable { 
    private Payable obj;
    public $Proxy0(Payable obj) {
        this.obj = obj;
    }
​
    @Override
    public void pay(){
        System.out.println("列印日誌1");
        obj.pay();
        System.out.println("列印日誌2");
    }
​
}

陀螺解釋說:“雖然generateSrc方法看起來很麻煩,但是生成的最終結果卻很容易理解,就是生成一個實現了某個介面的類,並在重寫介面所有方法的過程前後新增了日誌邏輯。” ​

“邏輯我理解了,只不過對generateSrc的程式碼還有點暈。我就暫時先不理會generateSrc的細節了,先把握整體思路。我有兩個問題,首先,我看到自動生成的類名由SiShiDaDaoLogProxy變成了$Proxy0,這是為什麼?”招財丟擲了第一個問題。 ​

“好眼力。在代理物件生成的過程中你會發現,我們從始至終都沒有用到過這個類的名字,所以名字叫什麼其實無所謂。此外,動態代理根據我們傳入引數的不同會返回不同的代理物件,所以我乾脆就起了一箇中性一點的名字Proxy0。至於為什麼用$開頭,因為JDK有個規範,在ClassPath下只要是$開頭的.class檔案,一般都是自動生成的,我只是遵照了一下這個規範罷了。” ​

“第二個問題,目前這個版本的功能是要得到實現了任意介面的類的代理,並且當客戶端傳入的介面物件是Payable.class時,也得到了我們期望的執行結果。但是我認為這只是恰好傳入的引數是Payable.class罷了,如果傳入的其他介面類,比如Comparable.class,我不認為客戶端能呼叫成功,因為newProxyInstance方法進行物件例項化時傳遞的引數是new SiShiDaDao()。”招財指了指程式碼。 ​

// 引數被寫死了
Payable p = (Payable) proxyConstructor.newInstance(new SiShiDaDao());

“而當引數是Comparable.class的時候,我們需要傳入的應該是實現了Comparable介面的物件例項。我說的對不,師傅。”招財幸災樂禍地問。

招財的成長讓陀螺大感吃驚,笑了笑說:“你說的沒錯,如果傳入的引數不是Payable.class,雖然能夠生成我們期望的程式碼,但是沒辦法執行,原因正如你剛才所說。不僅如此,目前自動生成的代理類只能新增固定的日誌邏輯,我們希望這個邏輯能讓使用者自己定義。” ​

“所以,第3個版本要來了吧。”招財摩拳擦掌,已經迫不及待地聽陀螺繼續講下去了。 ​

“沒錯!” ​

v3.0——為實現了任意介面的類做任意代理

“想讓使用者可以自定義邏輯,那麼在呼叫newProxyInstance方法的時候自然應該多一個引數。很顯然,每個使用者傳入的邏輯都不一樣,但是引數卻只有一個,你想到了什麼?”陀螺問招財。 ​

“多型。這個引數應該是個介面或者高度抽象的類,使用者去實現介面或重寫方法來編寫自己的邏輯。” ​

“說得沒錯,這裡我們就用介面來實現。我把這個介面命名為InvocationHandler,並在裡邊定義一個方法invoke,使用者必須重寫這個方法來編寫自己的邏輯。” ​

public interface InvocationHandler {
​
    Object invoke(...) throws Throwable;
​
}

“我們的newProxyInstance方法的宣告也就變為了這樣。” ​

/**
 * @author 蟬沐風
 * @description 動態代理v3
 * @date 2022/1/14
 */
public class Proxy {
  
    ...
​
    public static Object newProxyInstance(ClassLoader classLoader, Class intfce, InvocationHandler handler) {
       
        ...
            
    }
    
    ...
 
}

“接下來我們需要確定invoke方法中的引數,”陀螺繼續說道,“因為我們要在方法前後新增邏輯,所以使用者實現InvocationHandler介面並重寫invoke方法時,其中的程式碼結構應該是這個樣子。”說罷,陀螺給出了程式碼。 ​

public class LogInvocationHandler implements InvocationHandler {
    
    @Override
    public Object invoke(...) throws Throwable {
        // 方法呼叫之前的邏輯處理
        before();
​
        //在此進行實際方法呼叫
        ...
​
        // 方法呼叫之後的邏輯處理
        after();
    }
    
    private void before() {
        System.out.println("列印日誌1");
    }
​
    private void after() {
        System.out.println("列印日誌2");
    }
}

陀螺接著說:“我們需要在beforeafter方法中間呼叫某個方法,可以傳入Method物件,這樣就可以利用反射來呼叫這個方法了,因此invoke方法中至少應該包含Method物件和方法的引數,像這樣invoke(Method m, Object[] args)。” ​

招財提出了一個問題:“但是反射呼叫方法的時候還需要知道呼叫的是哪個物件的方法,這個引數該怎麼得到呢?”

陀螺回答道:“這個好辦,我們可以在實現InvocationHandler的時候,建立一個構造器,通過建構函式的方式傳入被代理物件,如此一來程式碼就變成了這樣。” ​

public class LogInvocationHandler implements InvocationHandler {
​
    // 被代理物件
    private Object target;
​
    public LogInvocationHandler(Object target) {
        this.target = target;
    }
​
    @Override
    public Object invoke(Method m, Object[] args) throws Throwable {
        before();
        Object res = m.invoke(target, args);
        after();
​
        return res;
    }
​
    private void before() {
        System.out.println("列印日誌1");
    }
​
    private void after() {
        System.out.println("列印日誌2");
    }
​
}

看到這裡,招財已經兩眼放光了,大叫:“我知道了!現在我們重寫的invoke方法中其實已經包含了最完整的邏輯,而且這個物件也會作為引數被傳入到newProxyInstance方法中,也就是說,在之後自動生成的代理物件中只要呼叫LogInvocationHandler例項物件的invoke方法,然後把Method引數和Object[]引數傳入就可以了!” ​

看著招財興奮的樣子,陀螺也忍不住樂起來,“哈哈哈,沒錯!你已經說出了動態代理的核心思想了。現在拋開newProxyInstance函式內部的實現細節,客戶端該怎麼呼叫我們已經完成的封裝?” ​

“首先我們需要建立一個被代理物件,這裡就以SiShiDaDao的例項物件為例吧;其次,實現InvocationHandler介面重寫invoke方法,建立自己的邏輯;再次,呼叫Proxy.newProxyInstance方法,得到代理物件;最後呼叫代理物件的目標方法就可以了。”招財回答得很流利。 ​

public class Client {
    public static void main(String[] args) {
​
        // 建立被代理物件
        SiShiDaDao target = new SiShiDaDao();
​
        // 實現自己的邏輯
        InvocationHandler logHandler = new LogInvocationHandler(target);
        
        // 得到代理物件
        Payable proxy = (Payable) Proxy.newProxyInstance(new MyClassLoader(), Payable.class, logHandler);
        
        // 呼叫代理物件目標方法
        proxy.pay();
    }
}

“我寫的程式碼沒錯吧師傅。”招財一臉得意,“接下來是不是可以看看newProxyInstance方法的實現細節了?” ​

陀螺擺擺手,“別急!在瞭解newProxyInstance的細節之前,你需要先明白newProxyInstance自動生成的原始碼應該是什麼樣子,你試著寫一下,就用你剛剛寫的客戶端呼叫的引數。” ​

招財想了一下,給出了自己的程式碼。 ​

public class $Proxy0 implements Payable { 
    
    private InvocationHandler h;
    
    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }
​
    @Override
    public void pay(){
        
        Method m = Payable.class.getMethod("pay");
        this.h.invoke(m,new Object[]{});
        
    }
​
}

"嗯嗯,"陀螺點點頭,“大致的思路是對的,但是有幾點小問題。” ​

“您說說看。” ​

“第一,Payable應該寫成全限定類名designPattern.proxy.dynamicProxy.Payable,這樣無論傳入什麼介面型別,編譯的時候都不會有問題。” ​

“第二,在獲取Method的時候,你是傳入方法名來進行獲取的,這不夠。因為可能存在方法過載的情況,就是方法名相同但是方法引數不同。因此更好的做法是同時根據方法名和方法引數來獲取Method物件。” ​

“第三,pay()方法沒有捕獲異常,因為$Proxy0中的所有方法都用到了反射,需要進行異常捕獲。” ​

“那注意了這三點,是不是我就可以實現newProxyInstance細節了?”招財迫不及待地問。 ​

“沒錯,你現在已經完全有能力實現了,只不過需要加億點點細節!” ​

“億點點????”

陀螺說:“因為Payable介面中宣告的方法pay()很簡單,既沒有返回值,也沒有方法引數,所以需要在實現細節中考慮到有返回值和方法引數的情況。但是細節對你來說已經不重要了,因為你聽懂了原理就已經掌握了動態代理的精髓,我直接給你看程式碼吧!” ​

程式碼可能引起不適,可以直接跳過,或者訪問github獲取完整程式碼,自己跑一下效果更佳

/**
 * @author 蟬沐風
 * @description 動態代理v3
 * @date 2022/1/14
 */
public class Proxy {
​
    //定義換行符
    private static final String ln = "\r\n";
​
    public static Object newProxyInstance(ClassLoader classLoader, Class intfce, InvocationHandler h) {
​
        try {
​
            /** 1.生成原始碼 **/
            String src = generateSrc(intfce);
​
            /** 2.將原始碼寫入磁碟,生成.java檔案 **/
            File file = createJavaFile(src);
​
            /** 3.將生成的.java檔案編譯成.class檔案 **/
            compile(file);
​
            /** 4.類載入器將.class檔案載入到JVM **/
            Class proxyClass = classLoader.loadClass("$Proxy0");
            Constructor proxyConstructor = proxyClass.getConstructor(InvocationHandler.class);
            file.delete();
​
            return proxyConstructor.newInstance(h);
​
        } catch (Exception e) {
            e.printStackTrace();
        }
​
        return null;
​
    }
​
    private static String generateSrc(Class intfce) {
​
        //獲取介面所在包名
        String packageName = intfce.getPackage().getName() + "." + intfce.getSimpleName();
​
        StringBuilder sb = new StringBuilder();
        sb.append("package designPattern.proxy.dynamicProxy.v3;").append(ln)
                .append("import ").append(packageName).append(";").append(ln)
                .append("import java.lang.reflect.*;").append(ln)
                .append("public class $Proxy0 implements ").append(intfce.getName()).append(" { ").append(ln)
                .append("    private InvocationHandler h;").append(ln)
                .append("    public $Proxy0(InvocationHandler h) {").append(ln)
                .append("        this.h = h;").append(ln)
                .append("    }").append(ln).append(ln)
​
                .append(generateMethodsSrc(intfce))
​
                .append("}").append(ln).append(ln);
​
        System.out.println(sb.toString());
        return sb.toString();
    }
​
    private static StringBuilder generateMethodsSrc(Class intfce) {
        StringBuilder sb = new StringBuilder();
​
        for (Method m : intfce.getMethods()) {
            sb.append("    @Override").append(ln);
​
            Class<?>[] params = m.getParameterTypes();
            StringBuilder paramNames = new StringBuilder();
            StringBuilder paramValues = new StringBuilder();
            StringBuilder paramClasses = new StringBuilder();
​
            for (int i = 0; i < params.length; i++) {
                Class clazz = params[i];
                String type = clazz.getName();
                String paramName = toLowerFirstCase(clazz.getSimpleName()) + i;
                paramNames.append(type + " " + paramName);
                paramValues.append(paramName);
                paramClasses.append(clazz.getName() + ".class");
                if (i < params.length - 1) {
                    paramNames.append(",");
                    paramValues.append(",");
                    paramClasses.append(",");
                }
            }
​
            sb.append("    public ").append(m.getReturnType().getName()).append(" ").append(m.getName())
                    .append("(").append(paramNames).append("){").append(ln);
            sb.append("        try{").append(ln);
            sb.append("            Method m = ").append(intfce.getName()).append(".class.getMethod(").append(""" + m.getName() + "",").append("new Class[]{").append(paramClasses.toString()).append("});").append(ln);
            sb.append(hasReturnValue(m.getReturnType()) ? "            return " : "            ").append(getReturnCode("this.h.invoke(m,new Object[]{" + paramValues + "})", m.getReturnType())).append(";").append(ln);
​
            sb.append(getReturnEmptyCode(m.getReturnType()));
            sb.append("        }catch(Throwable e){}").append(ln);
            sb.append("    }").append(ln).append(ln);
​
        }
​
        return sb;
    }
​
    private static Map<Class, Class> mappings = new HashMap<Class, Class>();
​
    static {
        mappings.put(int.class, Integer.class);
    }
​
    private static String getReturnEmptyCode(Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "return 0;";
        } else if (returnClass == void.class) {
            return "";
        } else {
            return "return null;";
        }
    }
​
    private static boolean hasReturnValue(Class<?> clazz) {
        return clazz != void.class;
    }
​
    private static String getReturnCode(String code, Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "((" + mappings.get(returnClass).getName() + ")" + code + ")." + returnClass.getSimpleName() + "Value()";
        }
        return code;
    }
​
    private static String toLowerFirstCase(String src) {
        char[] chars = src.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
​
    private static File createJavaFile(String src) throws Exception {
        String filePath = Proxy.class.getResource("").getPath();
        File file = new File(filePath + "$Proxy0.java");
        FileWriter fw = new FileWriter(file);
        fw.write(src);
        fw.flush();
        fw.close();
        return file;
    }
​
    private static void compile(File file) throws IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
        Iterable iterable = manager.getJavaFileObjects(file);
        JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, iterable);
        task.call();
        manager.close();
    }
}
​

注:雖然程式碼處理了方法返回值和引數的問題,但是還有很多細節未完善,比如會重寫介面中的所有的方法,包括staticprivate方法,這顯然是不對的 ​

大家只需理解精神即可,這裡的細枝末節對我們並不重要

“現在,我們終於可以在實現了任意介面任意物件任意方法的前後新增自己的邏輯了!”招財興奮的喊道。 ​

陀螺笑了笑:“恭喜你,到此為止,你已經完全掌握了最難的設計模式——動態代理。現在你會發現,我們費盡心思設計的Proxy類和InvocationHandler介面再也不需要變動了。” ​

“是啊,那我們可以把這個功能封裝起來,然後在我們的專案裡用動態代理了。”招財有點激動。 ​

“雖然花了我們不少精力,但是得承認,我們目前完成的功能是不完善的。好在JDK為我們封裝了動態代理,其實我們一步步做的所有工作都是在模擬JDK提供的動態代理,包括介面和方法的名稱,都和JDK的動態代理一模一樣。但是在某一些引數上,我們和JDK的動態代理有一點差別。” ​

“哪些引數有區別?”招財問道。 ​

“我們設計的newProxyInstance方法和JDK的稍微有點區別,JDK的第二個引數是個陣列,不過這無關緊要,你只要知道這一點就行。” ​

// 我們設計的
Object newProxyInstance(ClassLoader classLoader,
                        Class intfce,
                        InvocationHandler h)
​
​
// JDK提供的
Object newProxyInstance(ClassLoader loader, 
                        Class<?>[] interfaces,
                        InvocationHandler h)

陀螺繼續說道:“還有一個引數比較重要,但是我們在當前版本中並沒有給出。甚至很多程式喵對JDK中的這個引數的存在意義都搞不清楚。” ​

這可徹底激發了招財的好奇心,“這個引數是什麼啊?” ​

陀螺明沒有直接回答招財,反而問道:“招財啊,我們目前實現的動態代理有什麼優點?有什麼缺點呢?” ​

招財不明所以,但是師傅既然問了,總得回答,“優點是,使用者可以不需要在意newProxyInstance的實現細節,只需要實現InvocationHandler介面,在invoke方法裡新增自己的邏輯,然後按照步驟就可以創造出自己的代理物件;硬要說缺點的話,那就是隻能在最後才能獲得代理物件,自己在invoke方法中定義邏輯的時候對代理物件毫無操作許可權。” ​

陀螺讚許的點點頭,“說到點子上了!雖然大部分使用者都不會直接在invoke中使用代理物件,但是為了功能的完善性,JDK提供了這個引數。接下來,我們稍微修改一下我們的程式碼,非常簡單。” ​

v4.0——終於完成對JDK動態代理的模擬

陀螺解釋說:“問題在於我們需要把生成的代理物件傳到invoke方法中,很顯然應該在newProxyInstance方法中做點文章。在自動生成程式碼的時候做一點改變,將this物件傳入invoke方法。”

@Override
public void pay(){
    try{
        Method m = designPattern.proxy.dynamicProxy.Payable.class.getMethod("pay",new Class[]{});
        this.h.invoke(this, m, new Object[]{});
    }catch(Throwable e){}
}

“這樣的話invoke方法的宣告也需要改變一下,改成invoke(Object proxy, Method m, Object[] args) ,對吧?”招財補充道。 ​

“沒錯,這樣在重寫invoke方法的時候,使用者就可以獲取到代理物件proxy,針對代理物件進行一系列操作就可以了。到此為止,我們完成了對JDK動態代理的模擬。” ​

後記

招財好奇地問:“師傅,JDK也和我們似的,通過拼接字串來得到代理物件的原始碼,然後再編譯嗎?” ​

陀螺哈哈大笑,“要真是這樣,JDK未免也太low了吧。JDK官方提供了Class位元組碼的規範,只要你知道這個規範,你可以直接按照這個規範編寫位元組碼檔案,從而跳過先生成.java,然後動態編譯成.class的過程。JDK動態代理就是在執行期生成位元組碼,直接寫Class位元組碼檔案的,這樣效率比較高。” ​

“師傅,你一開始就規定了必須使用介面來使用動態代理,是不是也和JDK的實現有關係啊。難道還有不是利用介面來實現動態代理的方式不成?”招財又又又一次丟擲了問題。 ​

陀螺對自己的弟子是又愛又恨,“你這傢伙還真是敏銳,除了JDK動態之外還有CGLib動態代理,前者通過介面實現,後者通過繼承實現,但是別想讓我繼續給你講CGLib了,講完JDK動態代理我半條命都快沒了。下次吧!” ​

PS:我也實在不想讓陀螺回答下去了,畢竟寫這篇文章快兩個周了......

強烈建議下載原始碼自己動手跑一下,關於動態代理的所有版本原始碼見github

image.png

相關文章