基於 CGLIB 庫的動態代理機制

YangAM發表於2018-07-05

之前的文章我們詳細的介紹了 JDK 自身的 API 所提供的一種動態代理的實現,它的實現相對而言是簡單的,但是卻有一個非常致命性的缺陷,就是隻能為介面中的方法完成代理,而委託類自己的方法或者父類中的方法都不可能被代理。

CGLIB 應運而生,它是一個高效能的,底層基於 ASM 框架的一個程式碼生成框架,它完美的解決了 JDK 版本的動態代理只能為介面方法代理的單一性不足問題,具體怎麼做的我們一起來看。

CGLIB 的動態代理機制

再詳細介紹 CGLIB 原理之前,我們先完整的跑起來一個例子吧,畢竟有目的性的學習總是不容易放棄的。

image

image

Student 類是我們的委託類,它本身繼承 Father 類並實現 Person 介面。

image

CGLIB 的攔截器有點像 JDK 動態代理中的處理器。

image

可以看到,CGLIB 建立的代理類是委託類的子類,所以可以被強轉為委託類型別。

image

從輸出結果可以看到,所有的方法都得到了代理。

image

這算是 CGLIB 的一個最簡單應用了,大家不妨複製程式碼自己執行一下,接著我們會一點點來分析這段程式碼。

我們首先來看看 CGLIB 生成的代理類具有什麼樣的結構,通過設定系統屬性:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,本地磁碟路徑)
複製程式碼

可以指定 CGLIB 將動態生成的代理類儲存至指定的磁碟路徑下。接著我們反編譯一下這個代理類,有很多優秀的第三方反編譯工具,這裡我推薦給大家一個網站,該網站可以直接為我們反編譯一個 Class 檔案。

JAVA 反向工程網

於是你可以在你指定的磁碟目錄下找到 CGLIB 為你儲存下來的代理類,你只要將它上傳到這個網站上,就會得到該檔案反編譯後的 java 檔案。

首先看看這個代理類的繼承體系

image

Student 是我們需要代理的委託型別,結果生成的代理類就直接繼承了委託類。這一個小設計就完美的解決了 JDK 動態代理那個單一代理的缺陷,繼承了委託類,就可以反射出委託類介面中的所有方法,父類中的所有方法,自身定義的所有方法,完成這些方法的代理就完成了對委託類所有方法的代理。

Factory 介面中定義了幾個方法,用於設定和獲取回撥,也就是我們的攔截器,有關攔截器的部分待會說。

接著這部分,程式反射了父類,也就是是委託類,所有的方法,包括委託類的父類及父介面中的方法。

image

最後一部分,重寫了父類所有的方法,這裡以一個方法為例。

image

顯然,代理類重寫了父類中所有的方法,並且這些方法的邏輯也是很簡單的,將當前的方法簽名作為引數傳入到攔截器中,這裡也稱攔截器為『回撥』。

所以,從這一點來看,CGLIB 的方法呼叫是和 JDK 動態代理是類似的,都是需要依賴一個回撥器,只不過這裡我們稱為攔截器,JDK 中稱為處理器。

但是這裡我要提醒你的是,代理類中每一個方法都具有兩個版本,一個是原名重寫的方法,另一個是不經過攔截器的對應方法。這是 CGLIB 中 FastClass 機制的一個結果,這裡我只想引起你的注意而已,有關 FastClass 待會會介紹。

至此,我們研究了代理類的基本結構,大體上是類似於 JDK 動態代理的,不同點在於,CGLIB 生成的代理類直接繼承我們的委託類以至於能夠代理委託類中所有的方法。

既然代理類中所有的方法呼叫都會轉交攔截器,那麼我們就來看看這個攔截器的各個引數都代表什麼意思。

image

自定義攔截器很簡單,只需要實現我們 MethodInterceptor 介面並重寫其 intercept 方法即可。這個方法有四個引數,我們分別看看都代表著什麼。

  • obj:它代表的是我們代理類的例項物件
  • method:當前呼叫方法的引用
  • arg:呼叫該方法的形式引數
  • proxy:它也代表著當前方法的引用,基於 FastClass 機制

我們知道 Method 是基於反射來呼叫方法的,但是反射的效率總是要低於直接的方法呼叫的,而 MethodProxy 基於 FastClass 機制對方法直接下標索引,並通過索引直接定位和呼叫方法,是一點效能上的提升。

我們看一個 MethodProxy 例項的工廠方法原始碼:

public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
    MethodProxy proxy = new MethodProxy();
    proxy.sig1 = new Signature(name1, desc);
    proxy.sig2 = new Signature(name2, desc);
    proxy.createInfo = new MethodProxy.CreateInfo(c1, c2);
    return proxy;
}
複製程式碼

其中,形式引數 desc 代表的是一個方法的方法描述符,c1 代表的是這個方法所屬的類,值一般是我們的委託類,c2 代表的值往往是我們生成的代理類。而 name1 是委託類中該方法的方法名,name2 是代理類中該方法的方法名。

舉個例子:

var1 = Class.forName("Main.Student");
var0 = Class.forName("Main.Student$$EnhancerByCGLIB$$56e20d66");
MethodProxy.create(var1, var0, "()V", "sayHello", "CGLIB$sayHello$3");
複製程式碼

var1 是我們的委託類,var0 是該委託類的代理類,「()V」是 sayHello 方法的方法簽名,「CGLIB$sayHello$3」是 sayHello 方法在代理類中的方法名。

有了這幾個引數,MethodProxy 就可以初始化一個 FastClassInfo。

private static class FastClassInfo {
    FastClass f1;
    FastClass f2;
    int i1;
    int i2;
    private FastClassInfo() {
    }
}
複製程式碼

而 FastClass 是個什麼呢,其實內部是有點複雜的,這裡簡單給大家說一下。

FastClass 有點裝飾者模式的意思,內部包含一個 Class 物件,並且會對其中所有的方法進行一個索引標記,於是外部對於任意方法的呼叫只需要提供一個索引值,FastClass 就能夠快速定位到具體的方法。

而這裡的 f1 內部包裝的會是我們的委託類,f2 則會包裝我們的代理類,i1 是當前方法在 f1 中的索引值,i2 是當前方法在 f2 中的索引值。

所以,基於 FastClass 的方法呼叫也是簡單的,invoke 方法中指定一個索引即可,而不需要傳統的反射方式,需要給 invoke 方法傳入呼叫者,然後在通過反射呼叫的該方法進行呼叫。

總的來說,一個 MethodProxy 例項會對應兩個 FastClass 例項,一個包裝了委託類,並且暴露了該方法索引,另一個包裝了代理類,同樣暴露了該方法在代理類中的索引。

好,現在考大家一下:

image

MethodProxy 中 invoke 方法和 invokeSuper 方法分別呼叫的是哪個方法?代理類中的?還是委託類中的?

答案是:invoke 方法會呼叫後者,invokeSuper 則會呼叫前者。

image

可能很多人還是有點繞,其實很簡單,一個 FastClass 例項會繫結一個 Class 型別,並且會對該 Class 中所有的方法進行一個索引標記。

那麼按照我們說的,f1 繫結的是我們的委託類,f2 繫結的是我們的代理類,而無論你是用 f1 或是 f2 來呼叫這個 invoke 方法,你都是需要傳入一個 obj 例項的,而這個例項就是我們的代理類例項,由於 f1.i1 對應的方法簽名是 「public final void run」,而 f2.i2 對應的方法簽名則是「final void CGLIB$0」。

所以,f1.i1.invoke 和 f2.i2.invoke 呼叫的是同一個例項的不同方法,這也說明了為什麼 CGLIB 搞出來的代理類每種方法都有兩個形式的原因,但個人覺得這樣的設計有點無用功,還容易造成死迴圈,增加理解難度。

而這個 FastClass 的 invoke 方法也沒那麼神祕:

image

不要想太複雜,一個 FastClass 例項只不過掃描了內部 Class 型別的基本方法後,在 invoke 方法中列出 switch-case 選項,而每一次 invoke 的呼叫都是先匹配一下索引,然後讓目標物件直接呼叫目標方法。

所以這裡會引發一個問題,死迴圈的問題。我們的攔截器一般都是這樣寫的:

System.out.println("Before:" + method);
Object object = proxy.invokeSuper(obj, arg);
System.out.println("After:" + method);
return object;
複製程式碼

invokeSuper 會呼叫 「final void CGLIB$0」方法,間接呼叫委託類的對應方法。而如果你改成 invoke,像這樣:

System.out.println("Before:" + method);
Object object = proxy.invoke(obj, arg);
System.out.println("After:" + method);
return object;
複製程式碼

結果就是死迴圈,為什麼呢?

invoke 方法呼叫的是和委託類中方法具有一樣簽名的方法,最終走到我們的代理類裡面,就會再經過一次攔截器,而攔截器又不停的回撥,它倆就在這死迴圈了。

至此,我覺得對於 CGLIB 的基本原理我已經介紹完了,你需要整理一下邏輯,理解它從頭到尾的執行過程。

CGLIB 的不足

我們老說,CGLIB 解決了 JDK 動態代理的致命問題,單一的代理機制。它可以代理父類以及自身、父介面中的方法,但是你注意一下,我沒有說所有的方法都能代理

CGLIB 的最大不足在於,它需要繼承我們的委託類,所以如果委託類被修飾為 final,那就意味著,這個類 CGLIB 代理不了。

自然的,即便某個類不是 final 類,但是其中如果有 final 修飾的方法,那麼該方法也是不能被代理的。這一點從我們反射的原始碼可以看出來,CGLIB 生成的代理類需要重寫委託類中所有的方法,而一個修飾為 final 的方法是不允許重寫的。

總的來說,CGLIB 已經非常的優秀了,瑕不掩瑜。幾乎市面上主流的框架中都不可避免的使用了 CGLIB,以後會帶大家分析框架原始碼,到時候我們再見 CGLIB !


文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:OneJavaCoder,所有文章都將同步在公眾號上。

image

相關文章