基於 CGLIB 庫的動態代理機制

以前的文章咱們詳細的介紹了 JDK 自身的 API 所提供的一種動態代理的實現,它的實現相對而言是簡單的,可是卻有一個很是致命性的缺陷,就是隻能爲接口中的方法完成代理,而委託類本身的方法或者父類中的方法都不可能被代理。java

CGLIB 應運而生,它是一個高性能的,底層基於 ASM 框架的一個代碼生成框架,它完美的解決了 JDK 版本的動態代理只能爲接口方法代理的單一性不足問題,具體怎麼作的咱們一塊兒來看。git

CGLIB 的動態代理機制

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

image

image

Student 類是咱們的委託類,它自己繼承 Father 類並實現 Person 接口。bash

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
相關文章
相關標籤/搜索