之前的文章我們詳細的介紹了 JDK 自身的 API 所提供的一種動態代理的實現,它的實現相對而言是簡單的,但是卻有一個非常致命性的缺陷,就是只能為接口中的方法完成代理,而委托類自己的方法或者父類中的方法都不可能被代理。
CGLIB 應運而生,它是一個高性能的,底層基於 ASM 框架的一個代碼生成框架,它完美的解決了 JDK 版本的動態代理只能為接口方法代理的單一性不足問題,具體怎么做的我們一起來看。
CGLIB 的動態代理機制
再詳細介紹 CGLIB 原理之前,我們先完整的跑起來一個例子吧,畢竟有目的性的學習總是不容易放棄的。
Student 類是我們的委托類,它本身繼承 Father 類並實現 Person 接口。
CGLIB 的攔截器有點像 JDK 動態代理中的處理器。
可以看到,CGLIB 創建的代理類是委托類的子類,所以可以被強轉為委托類類型。
從輸出結果可以看到,所有的方法都得到了代理。
這算是 CGLIB 的一個最簡單應用了,大家不妨復制代碼自己運行一下,接着我們會一點點來分析這段代碼。
我們首先來看看 CGLIB 生成的代理類具有什么樣的結構,通過設置系統屬性:
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,本地磁盤路徑)
可以指定 CGLIB 將動態生成的代理類保存至指定的磁盤路徑下。接着我們反編譯一下這個代理類,有很多優秀的第三方反編譯工具,這里我推薦給大家一個網站,該網站可以直接為我們反編譯一個 Class 文件。
於是你可以在你指定的磁盤目錄下找到 CGLIB 為你保存下來的代理類,你只要將它上傳到這個網站上,就會得到該文件反編譯后的 java 文件。
首先看看這個代理類的繼承體系:
Student 是我們需要代理的委托類型,結果生成的代理類就直接繼承了委托類。這一個小設計就完美的解決了 JDK 動態代理那個單一代理的缺陷,繼承了委托類,就可以反射出委托類接口中的所有方法,父類中的所有方法,自身定義的所有方法,完成這些方法的代理就完成了對委托類所有方法的代理。
Factory 接口中定義了幾個方法,用於設置和獲取回調,也就是我們的攔截器,有關攔截器的部分待會說。
接着這部分,程序反射了父類,也就是是委托類,所有的方法,包括委托類的父類及父接口中的方法。
最后一部分,重寫了父類所有的方法,這里以一個方法為例。
顯然,代理類重寫了父類中所有的方法,並且這些方法的邏輯也是很簡單的,將當前的方法簽名作為參數傳入到攔截器中,這里也稱攔截器為『回調』。
所以,從這一點來看,CGLIB 的方法調用是和 JDK 動態代理是類似的,都是需要依賴一個回調器,只不過這里我們稱為攔截器,JDK 中稱為處理器。
但是這里我要提醒你的是,代理類中每一個方法都具有兩個版本,一個是原名重寫的方法,另一個是不經過攔截器的對應方法。這是 CGLIB 中 FastClass 機制的一個結果,這里我只想引起你的注意而已,有關 FastClass 待會會介紹。
至此,我們研究了代理類的基本結構,大體上是類似於 JDK 動態代理的,不同點在於,CGLIB 生成的代理類直接繼承我們的委托類以至於能夠代理委托類中所有的方法。
既然代理類中所有的方法調用都會轉交攔截器,那么我們就來看看這個攔截器的各個參數都代表什么意思。
自定義攔截器很簡單,只需要實現我們 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 實例,一個包裝了委托類,並且暴露了該方法索引,另一個包裝了代理類,同樣暴露了該方法在代理類中的索引。
好,現在考大家一下:
MethodProxy 中 invoke 方法和 invokeSuper 方法分別調用的是哪個方法?代理類中的?還是委托類中的?
答案是:invoke 方法會調用后者,invokeSuper 則會調用前者。
可能很多人還是有點繞,其實很簡單,一個 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 方法也沒那么神秘:
不要想太復雜,一個 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,所有文章都將同步在公眾號上。