「Java」靜態代理、動態代理及 $Proxy0 類源碼分析

從前從前,有個面試官問我動態代理和靜態代理的區別,我當時支支吾吾沒說清楚,只提到了動態代理須要實現InvocationHandler接口,而後使用Proxy類反射建立實例云云。至於靜態代理……這玩意不就是一種設計思想?java

面試官笑了笑,今後天涯路人不相逢。程序員

我痛定思痛,必定要把代理這一塊搞懂,因而乎有了這篇文章。之後不再怕面試官問我關於靜態代理和動態代理的問題了!面試

1. 什麼是代理

說到代理,就不得不提設計模式中的代理模式,代理模式就是對代理思想的一種設計模式實現。數據庫

百度百科對於代理模式的定義是這樣的:設計模式

爲其餘對象提供一種代理以控制對這個對象的訪問。在某些狀況下,一個對象不適合或者不能直接引用另外一個對象,而代理對象能夠在客戶端和目標對象之間起到中介的做用。

在這段定義中有這樣兩個字:中介。app

我想下面這個例子能夠比較好的解釋代理模式。ide

相信在一個陌生的城市打拼的程序員們在初期都會遇到這樣一個問題:租房。咱們一般有三種方式,第一能夠本身在閒魚、豆瓣、自如等信息網站去找房源,第二直接去心儀的小區公告欄看看有沒有招租信息(固然可能被中介的廣告霸佔),第三就是聯繫房產中介,中介會幫你挑選你想要租的房子,只不過須要付一筆服務費。工具

2020052803

假設我選擇委託中介來租房,在這個過程當中就能夠把房子抽象爲一個類,這是我最終想要獲得的東西。而後把幫我租房的中介抽象爲一個類,經過委託中介,我能夠獲得本身想要的房子。同時,這兩個類實現了相同的接口,能夠這麼去理解這裏相同接口的做用:房子經過接口註冊在數據庫中,中介經過接口找到了註冊在數據庫中的房子。源碼分析

而我委託中介幫我找房子的這個過程,就是代理。測試

2. 定義與類圖

在根據上面說的租房的例子來編寫實際的代碼做爲靜態代理的示例以前,首先得了解一下代理模式中的幾個角色,代理模式中有三個主要角色,即抽象主題角色、真實角色(被代理角色)以及代理類角色。

2.1. 主題角色 (Subject)

主題角色能夠是接口,也能夠是抽象類。它定義了真實角色和代理類角色共有的方法,主題類讓這二者具備一致性。

同時,也正是基於主題角色,才能實現代理的功能。

2.2. 真實角色 (RealSubject)

真實角色就是被代理類,在上面的例子中就是房子,同時也是具體業務邏輯的執行者。

2.3. 代理類角色 (Proxy)

代理類角色的內部含有對真實角色 RealSubject 的引用,它負責對被代理角色的調用,並在被代理角色處理先後作預處理和後處理。

在上面租房的例子中就是房產中介。

2.4. Client

有人可能會問了,那「我」呢?簡單點說,其實「我」就是測試方法中的 main 方法,負責調用代理角色的方法。

2.5 類圖

2020052804

圖片來源於 Proxy模式——靜態代理

3. 靜態代理

所謂的靜態代理,就是在程序啓動以前代理類的 .class 文件就已經存在。而代理類多是程序員直接建立的 .java 文件,或者是藉助某些工具生成的 .java 文件,但無一例外都必須再由編譯器編譯成 .class 文件以後再啓動程序。

3.1. 靜態代理實現

基於上面租房的例子使用代碼實現。

首先建立主題角色,它是一個接口,這個接口擁有一個方法,而這個方法是須要被其餘兩個角色重寫的。

public interface Subject {

    /**
     * 各個角色的公用方法
     */
    void job();
}

而後是真實角色,也就是被代理角色、真正的業務邏輯執行者。

public class House implements Subject {

    @Override
    public void job() {
        System.out.println("我是客戶想要的房子,經過 job 方法註冊在數據庫中");
    }
}

而後代理類,它可以加強被代理角色的方法。代理類就是幫助我」找到好房子「的房產中介。

當一個房產中介擁有一個客戶(RealSubject)時,纔會發揮他的做用,在個人」意圖「被實現先後,它分別能夠對個人」意圖「進行加強。

public class Proxy implements Subject {

    private RealSubject realSubject;

    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void job() {
        System.out.println("我是中介,我會在數據庫中檢索,幫助客戶找到心儀的房子");
        house.job();
        System.out.println("我是中介,找到了數據庫中符合客戶需求的方法");
    }
}

最後,「我」出場了,「我」委託中介尋找房子。

public class Client {
    public static void main(String[] args) {
        // 我可能心儀的房子
        House house = new House();
        // 代理類——房產中介
        Proxy proxy = new Proxy(house);
        // "我"委託中介去尋找房子
        proxy.job();
    }   
}

最終「我」執行的是代理類(中介)的 job 方法,因爲代理類持有一個真實角色(房子),程序又會執行真實角色的 job 方法,這樣就實現了「我」委託中介找到房子的靜態代理過程。

3.2. 靜態代理的優缺點

3.2.1. 優勢

  1. 業務類只須要關注業務邏輯自己,保證了業務類的重用性。
  2. 客戶端只須要知道代理,無需關注具體實現。
中介只須要關注本身能找房子的效率和質量就能夠了,不管誰想來委託中介,都能找到房子。而「我」不須要知道中介是如何找房子的,只要他幫我找到房子,就能夠了。

3.2.2. 缺點

  1. 因爲代理類和被代理類都實現了主題接口,它們都有相同的方法,致使大量代碼重複。同時若是主題接口新增了一個方法,那麼代理類與被代理類也都須要實現這個方法,增長了維護代碼的複雜度。
  2. 若是代理類要爲其餘真實角色提供委託服務的話,就須要實現其餘的接口,當規模變大時也會增長代碼複雜度。
若是中介不只提供租房服務,還提供打遊戲、賣房子、賣電影票、賣彩票、陪聊天、陪玩遊戲等等一系列服務,那麼他將變得無比龐雜,沒有人敢動他(這裏的他指代碼)。

4. 動態代理

上面討論的是靜態代理,接下來再聊聊動態代理。那麼什麼是動態代理呢?

所謂的動態代理,就是在程序運行時建立代理類的代理方式。而這也是靜態代理和動態代理的區別。

4.1. 動態代理實現

既然是在程序運行時生成的代理類,那麼必然須要藉助其餘的工具來生成,而在 Java 中就是經過 java.lang.reflect.Proxy 類來生成代理類的。同時,還須要實現InvocationHandler接口來實現方法調用,下面就用代碼來實現動態代理。

一樣是上面租房的例子,接口Subject不變,被代理類House也不變,須要新建一個動態代理類。

public class DynamicProxy implements InvocationHandler {

    private Object target;

    public DynamicProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("被代理的類:" + proxy.getClass());
        System.out.println("被代理的類的執行方法:" + method.getName());
        Object object = method.invoke(target, args);
        System.out.println("被代理的類的方法執行完成");
        return object;
    }
}

動態代理類實現了InvocationHandler接口,同時與靜態代理同樣,在它內部也持有一個對象,這個對象正是被代理對象,而後在代理類的invoke方法中調用具體的方法。

最後再客戶端,也就Client角色中編寫測試代碼。

import java.lang.reflect.Proxy;

public class Client {

    public static void main(String[] args) {
    // 我可能心儀的房子
    Subject subject = new House();
    // 代理類——房產中介
    DynamicProxy dynamicProxy = new DynamicProxy(subject);
    // 獲取代理類
    Subject proxyInstance = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(),
            subject.getClass().getInterfaces(),
            dynamicProxy);
    // "我"委託中介去尋找房子
    proxyInstance.job();
    }
}

與靜態代理不一樣的是,咱們須要經過Proxy.newProxyInstance方法來實例化動態生成的代理類,而這個方法中的參數分別表明的意義是:

  • ClassLoader loader: 被代理類的類加載器
  • Class<?>[] interfaces: 被代理類實現的接口
  • InvocationHandler h: 實現指定接口InvocationHandler的實現類

須要這三個參數的緣由是:須要經過與被代理類相同的類加載器去加載動態生成的代理類,同時代理類須要實現與被代理類相同的接口,最後須要經過實現指定接口InvocationHandler的實現類來完成代理調用方法的功能。

最終的輸出結果是:

被代理的類:class com.sun.proxy.$Proxy0
被代理的類的執行方法:job
我是客戶想要的房子,經過 job 方法註冊在數據庫中
被代理的類的方法執行完成

咱們能夠看到生成的代理類是com.sun.proxy.$Proxy0,經過動態代理它完成了與靜態代理同樣的委託任務。

4.2. 動態代理的優缺點

與靜態代理相比,動態代理還具備不須要本身寫代理類的優勢,由於代理類時運行時程序自動生成的。

同時,動態代理的必須先實現InvocationHandler接口,而後使用Proxy類中的newProxyInstance方法動態的建立代理類,這就致使了動態代理只能代理接口。

5. 動態代理類源碼分析

上文說到運行時生成的動態代理類會繼承於java.lang.reflect.Proxy類,這是爲何呢?

5.1. 獲取動態代理類源碼

咱們能夠經過設置系統參數來保存動態生成的代理類。

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

你問我是怎麼知道這個參數的?我也不知道,是 jdk 源碼裏面寫死的。

有興趣的同窗能夠跟蹤一下Proxy.newProxyInstance這個方法,在通過數次跳轉後,你就能找到這個系統參數了,下面給出調用鏈。

Proxy.newProxyInstance
->getProxyClass0
->proxyClassCache.get(loader, interfaces)
->subKeyFactory.apply(key, parameter)
->ProxyClassFactory.apply
->ProxyGenerator.generateProxyClass
->saveGeneratedFiles
->private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))

你問我怎麼知道調用鏈是這樣的?看註釋啊...

在開啓saveGeneratedFiles參數後,咱們會發如今項目中多出了com.sun.proxy.$Proxy0類,打開它就是生成的動態代理類源碼。

public final class $Proxy0 extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;
    // equals 和 hashCode 方法省略...

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void job() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("org.planeswalker.proxy.statical.Subject").getMethod("job");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

5.2. 爲何要重寫equalstoStringhashCode方法

能夠看到,在動態代理類中有四個私有靜態成員變量,結合 static 代碼塊,咱們知道這四個Method分別表明了equalstoStringjobhashCode方法。

job方法很好理解,由於這是我須要動態代理類去調用被代理類的方法。而另外三個方法,爲何須要重寫?

從源碼中能夠看到,這三個方法實際是調用了InvocationHandler接口實現類的相應方法。而咱們知道動態代理類其實至關於一箇中間件,經過動態代理類咱們實際想要調用的是被代理類的方法,這麼一想就很好理解了——重寫這三個方法的緣由是爲了讓動態代理類與被代理類劃上」≈「號。

若是沒有重寫這三個方法,那麼它們的hashcodetoString將會返回不一樣值,這樣實現的動態代理類也就不完善了。

爲何說是」≈「號而不是」=「號呢?由於動態代理類實際是一個com.sun.proxy.$Proxy0類,雖然它具備與被代理類相同的狀態(包括大部分方法與屬性),但實際上這兩個類經過equals方法來比較返回的會是 false,由於它們的內存地址是不同的。

被代理類未重寫 equals方法,因此調用的是 Object#equals,而這裏比較的是內存地址。

5.3 爲何動態代理類要繼承 Proxy 類

這個問題其實應該去問 jdk 的實現者,這是他們規定的,哪來的爲何?

我也去網上搜索了不少相關的問題,大部分仍是指向了一個答案——繼承 Proxy 類能夠減小代碼的冗餘度。

在上面給出的動態生成的代理類源碼中咱們能夠知道,動態代理類其實只是作了一個轉發,調用的仍是被代理類的方法。若是咱們將被代理類的屬性和方法都寫在動態代理類中,而在調用方法時依舊是經過轉發,那麼這些被繼承的屬性和方法其實是沒有用到的,這對於內存空間來講是一種浪費。

6. 小結

本文講述了代理模式、代理模式中的角色、靜態代理代碼實現以及優缺點、靜態代理與動態代理的區別、動態代理代碼實現及優缺點。

同時還提出了兩個問題以及我對這兩個的理解。

但願能夠幫助到你們。

以上。

相關文章
相關標籤/搜索