Byte Buddy 教程


https://notes.diguage.com/byte-buddy-tutorial/

 

官網及版本庫

本文檔的版本庫使用 Git 管理。另外,單獨發布閱讀版。

“地瓜哥”博客網

http://www.diguage.com/ 。D瓜哥的個人博客。歡迎光臨,不過,內容很雜亂,請見諒。不見諒,你來打我啊,😂😂

本文檔官網

http://notes.diguage.com/byte-buddy-tutorial/ 。為了方便閱讀,這里展示了處理好的文檔。閱讀請點擊這個網址。

本文檔版本庫

https://github.com/diguage/byte-buddy-tutorial 。反正也沒啥保密可言,版本庫分分鍾給出。😜

  文本中斜體字表示翻譯很不滿意,還需要再三斟酌來探究更好的翻譯。希望感興趣的小伙伴在閱讀的時候多多留意。更希望發送 PR 過來。謝謝!!

為什么需要在運行時生成代碼?

Java 語言帶有一套比較嚴格的類型系統。Java 要求所有變量和對象都有一個確定的類型,並且任何賦值不兼容類型的嘗試都會拋出錯誤。這些錯誤通常都會被編譯器檢查出來,或者極少情況下,在非法轉換類型的時候由Java運行時拋出。如此嚴格的類型限制在大多數情況下是可取的,比如在編寫業務應用時。在業務領域,通常可以以明確的方式去描述其中任何元素,各個元素都有自己明確的類型。通過這種方式,我們可以用 Java 構建具有非常強可讀性和穩定性的應用,應用中的錯誤也非常貼近源碼。除此之外,Java 嚴格的類型系統造就 Java 在企業編程中的普及。

然而,通過強制實施其嚴格的類型系統,Java 限制了自己在其他領域的應用范圍。 比如,當編寫一個供其他 Java 應用使用的通用庫時,我們通常不能引用用戶應用中定義的任何類型,因為當這個通用庫被編譯時,我們還不知道這些類型。為了調用用戶未知代碼的方法或者訪問其屬性,Java 類庫提供了一套反射 API。使用這套反射 API,我們就可以反省未知類型,進而調用方法或者訪問屬性。不幸的是,這套反射 API 的用法有兩個明顯的缺點:

  • 相比硬編碼的方法調用,使用 反射 API 非常慢:首先,需要執行一個相當昂貴的方法查找來獲取描述特定方法的對象。同時,當一個方法被調用時,這要求 Java 虛擬機去運行本地代碼,相比直接調用,這需要一個很長的運行時間。然而,現代 Java 虛擬機知道一個被稱為“類型膨脹”的概念:基於 JNI 的方法調用會被動態生成的字節碼給替換掉,而這些方法調用的字節碼被注入到一個動態生成的類中。(即使 Java 虛擬機自身也使用代碼生成!)畢竟,Java 的類型膨脹系統仍存在生成非常一般的代碼的缺點,例如,僅能使用基本類型的裝箱類型以至於性能缺陷不能完全解決。

  • 反射 API 能繞過類型安全檢查:即使 Java 虛擬機支持通過反射進行代碼調用,但反射 API 自身並不是類型安全的。當編寫一個類庫時,只要我們不需要把反射 API 暴露給庫的用戶,就不會有什么大問題。畢竟,當我們編譯類庫時,我們不知道用戶代碼,而且也不能校驗我們的庫與用戶類型是否匹配。有時,需要通過讓一個庫為我們自己調用我們自己的方法之一來向用戶顯示反射 API 示例。這是使用反射 API 變得有問題的地方,因為 Java 編譯器將具有所有信息來驗證我們的程序的類型安全性。例如,當實現方法級安全庫時,這個庫的用戶將希望這個庫做到強制執行安全限制才能調用方法。為此,在用戶傳遞過來方法所需的參數后,這個庫將反射性地調用方法。這樣,就沒有編譯時類型檢查這些方法參數是否與方法的反射調用相匹配。方法調用依然會校驗,只是被推遲到了運行時。這樣做,我們就錯失了 Java 編程語言的一大特性。

這正是運行時代碼生成能幫助我們的地方。它允許我們模擬一些只有使用動態編程語言編程才有的特性,而且不丟失 Java 的靜態類型檢查。這樣,我們就可以兩全其美並且還可以提高運行時性能。為了更好地理解這個問題,讓我們實現一個方法級安全庫。

編寫一個安全的庫

業務應用程序可能會增長,有時很難在我們的應用程序中概述調用堆棧。當我們在應用程序中使用至關重要的方法時,而這些方法只能在特定條件下調用,這可能會變得有問題。 設想一下,實現重置功能的業務應用程序可以從應用程序的數據庫中刪除所有內容。

class Service { void deleteEverything() { // delete everything ... } }

這樣的復位操作當然只能由管理員執行,而不是由應用程序的普通用戶執行。通過分析源代碼,我們當然可以確保這將永遠不會發生。但是,我們期望我們的應用能夠在未來發展壯大。因此,我們希望實現更緊密的安全模型,其中通過對應用程序的當前用戶的顯式檢查來保護方法調用。我們通常會使用一個安全框架來確保該方法從不被除管理員外的任何人調用。

為此,假設我們使用具有公共 API 如下的安全框架:

@Retention(RetentionPolicy.RUNTIME) @interface Secured { String user(); } class UserHolder { static String user; } interface Framework { <T> T secure(Class<T> type); }

在此框架中,Secured 注解應用於標記只能由給定用戶訪問的方法。UserHolder 用於在全局范圍內定義當前登錄到應用程序的用戶。Framework 接口允許通過調用給定類型的默認構造函數來創建安全實例。當然,這個框架過於簡單,但是,從本質上來說,即使流行的安全框架,例如 Spring Security,也是這樣實現的。這個安全框架的一個特點是我們過濾用戶的類型。通過調用我們框架的接口,我們承諾返回給用戶任何類型 T 的實例。幸虧這樣,用戶能夠透明地他自己的類型進行交互,就像安全框架根本不存在一樣。在測試環境中,用戶甚至可以創建其類型的不安全實例,使用這些實例來代替安全實例。你會同意這真的很方便!已知這種框架使用 POJO,普通的舊 Java 對象進行交互,這是一種用於描述不侵入框架的術語,這些框架不會將自己的類型強加給用戶。

現在,想象一下,假如我們知道傳遞給 Framework 的類型只能是 T = Service,而且 deleteEverything 方法用 @Secured("ADMIN") 注解。這樣,我們可以通過簡單的子類化來輕松實現這種特定類型的安全版本:

class SecuredService extends Service { @Override void deleteEverything() { if(UserHolder.user.equals("ADMIN")) { super.deleteEverything(); } else { throw new IllegalStateException("Not authorized"); } } }

通過這個額外的類,我們可以實現框架如下:

class HardcodedFrameworkImpl implements Framework { @Override public <T> T secure(Class<T> type) { if(type == Service.class) { return (T) new SecuredService(); } else { throw new IllegalArgumentException("Unknown: " + type); } } }

當然這個實現並沒有太多的用處。通過標注 secure 方法簽名,我們建議該方法可以為任何類型提供安全性,但實際上,一旦遇到其他事情,我們將拋出一個異常,然后是已知的 Service。此外,當編譯庫時,這將需要我們的安全庫知道有關此特定 Service 類型的信息。顯然,這不是實現框架的可行解決方案。那么我們如何解決這個問題呢?好吧,由於這是一個關於代碼生成庫的教程,你可能已經猜到答案:當通過調用 secure 方法, Service 類第一次被我們安全框架知道時,我們會在運行時后台地創建一個子類。通過使用代碼生成,我們可以使用任何給定的類型,在運行時將其子類化,並覆蓋我們要保護的方法。在我們的例子中,我們覆蓋所有被 @Secured 注解標注的方法,並從注解的 user 屬性中讀取所需的用戶。許多流行的 Java 框架都使用類似的方法實現。

基本信息

在學習代碼生成和 Byte Buddy 之前,請注意,應該謹慎使用代碼生成。Java 類型對於 Java 虛擬機來說,是相當特別的東西,通常不能當做垃圾被回收。因此,不應該過度使用代碼生成,而應該只在生成代碼是解決問題的唯一出路時使用。但是,如果需要像上面的示例那樣增強未知類型時,則代碼生成很可能是你唯一的選擇。用於安全性,事務管理,對象關系映射或類型模擬(mock)等框架是代碼生成庫的典型用戶。

當然,Byte Buddy 不是 Java 虛擬機上第一個代碼生成庫。不過,我們認為 Byte Buddy 擁有其他框架沒有的技巧。Byte Buddy 的總體目標是通過專注於其領域特定語言和注解的使用來聲明式地進行工作。據我們所知,沒有其他針對 Java 虛擬機的代碼生成庫以這種方式工作。不過,你可能希望看一下其他代碼生成框架,以找出最適合你的套件。以下庫在 Java 中很流行:

Java Proxy

Java 類庫自帶了一個代理工具,它允許為實現了一系列接口的類創建代理。這個內置的代理供應商非常方便,但局限性也特別明顯。 上面提到的安全框架就不能用這樣的方式來實現的,因為我們想擴展是類而不是擴展接口。

cglib

代碼生成庫(注:這里指 cglib)誕生於 Java 初期,但不幸的是沒有跟上 Java 平台的發展。然而,cglib 仍然是一個相當強大的庫,但其積極的開發卻變得相當模糊。鑒於此,其許多用戶已經離開了 cglib。

Javassist

該庫附帶一個編譯器,它使用包含 Java 源代碼的字符串,這些字符串在應用程序的運行時被轉換為 Java 字節碼。這是非常有前途的,本質上是一個好主意,因為 Java 源代碼顯然是描述 Java 類的好方法。但是,Javassist 編譯器在功能上比不了 javac 編譯器,並且在動態組合字符串以實現比較復雜的邏輯時容易出錯。此外,Javassist 還提供了一個類似於 Java 類庫中的代理工具,但允許擴展類,並不限於接口。然而,Javassist 的代理工具的范圍在其 API 和功能上仍然受到限制。

即使評估完這些框架,但我們相信 Byte Buddy 提供了功能和便利,可以減少徒勞地搜索。Byte Buddy 提供了一種具有表現力的領域特定語言,允許通過編寫簡單的 Java 代碼和使用強大的類型為你自己的代碼創建非常自定義的運行時類。與此同時,Byte Buddy 還具有非常開放的定制性,並不限制開箱即用的功能。如果需要,你甚至可以為任何實現的方法定義自定義字節碼。但即使不知道什么字節代碼是或它如何工作,你可以做很多,而不深入到框架。你有沒有看看 Hello World! example?,使用 Byte Buddy 是如此簡單。

當然,在選擇代碼生成庫時,一個愉快的 API 不是唯一需要考慮的特性。對於許多應用程序,生成代碼的運行時特性更有可能確定最佳選擇。而在生成的代碼本身的運行時間之外,用於創建動態類的運行時也是一個問題。聲稱“我們是最快的!”很容易,但是為庫的速度提供有效的評比指標卻很難。不過,我們希望提供這樣的指標作為基本方向。但是,請注意,這些結果並不一定會轉化為更具體的用例,此時你應該采用單獨的指標。

在討論我們的指標之前,讓我們來看一下原始數據。下表顯示了一個操作的平均運行時間,以納秒為單位,標准偏差在括號內附加:

  基線 Byte Buddy cglib Javassist Java proxy

簡單的類創建

0.003 (0.001)

142.772 (1.390)

515.174 (26.753)

193.733 (4.430)

70.712 (0.645)

接口實現

0.004 (0.001)

1'126.364 (10.328)

960.527 (11.788)

1'070.766 (59.865)

1'060.766 (12.231)

方法調用

0.002 (0.001)

0.002 (0.001)

0.003 (0.001)

0.011 (0.001)

0.008 (0.001)

類型擴展

0.004 (0.001)

885.983 (7.901)

1'632.730 (52.737)

683.478 (6.735)

5'408.329 (52.437)

父類方法調用

0.004 (0.001)

0.004 (0.001)

0.021 (0.001)

0.025 (0.001)

0.004 (0.001)

與靜態編譯器類似,代碼生成庫在生成快速代碼和快速生成代碼之間面臨着折衷。當在這些沖突的目標之間進行選擇時,Byte Buddy 的主要側重點在於以最少的運行時生成代碼。通常,類型創建或操作不是任何程序中的常見步驟,並不會對任何長期運行的應用程序產生重大影響;特別是因為類加載或類構建(class instrumentation)是運行此類代碼時最耗時且不可避免的步驟。

  按照這個邏輯,D瓜哥覺得應該選擇“生成快速代碼”,畢竟很少生成而且只生成一次,但是生成的代碼卻可能運行多次。不過,考慮到 Java 虛擬機的優化,選擇“生成快速代碼”是否是更好的選擇呢?

上表中的第一個基准測試測量一個庫在運行時子類化類,並且不實現或覆蓋任何方法。這給我們一個庫在代碼生成時的一般開銷的印象。在這個基准測試中,Java 代理執行得比其他庫更好,這是因為存在着一種優化,假設總是擴展接口。Byte Buddy 還會檢查類的泛型和注解類別,從而導致額外的運行時間。這個性能開銷在創建類的其他基准中也是可見的。基准(2a)展示了運行時創建類,這個類實現了一個有 18 個方法的接口;(2b)顯示為此類生成的方法的執行時間。類似地,(3a)顯示了擴展類的基准,這個擁有相同的 18 種被實現的方法。 Byte Buddy 提供了兩個基准測試,因為對於總是執行超類方法的攔截器來說,可能的優化是可能的。除了在類創建期間花費一段時間,Byte Buddy 創建類的執行時間通常達到基線,這意味着構建根本不會產生開銷。應該注意的是,如果元數據處理被禁用,則在類創建期間,Byte Buddy 也會勝過任何其他代碼生成庫。由於代碼生成的運行時間與程序的總運行時間相比微乎其微,所以這種性能優化是不可取的,因為它雖然獲得了極少的性能,但卻使庫代碼復雜很多。

最后,請注意,我們這些衡量 Java 代碼性能的測試,都由 Java 虛擬機即時編譯器優化過。如果你的代碼只能偶爾執行,那么性能將會比上述表格指標略差。在這種情況下,你的代碼並不是性能攸關的開始。這些性能測試代碼與 Byte Buddy 一起發布,你可以在自己的計算機上運行這些指標,其中可能會根據你的機器的處理能力對上述數字進行漲跌。因此,不要絕對地解釋上述數字,而是將它們視為不同庫的對比方式。當進一步開發 Byte Buddy 時,我們希望監控這些指標,以避免在添加新功能時造成性能損失。

在下面的教程中,我們將會逐步說明 Byte Buddy 的功能。我們將從其更廣泛的功能開始,這些功能最有可能被大多數用戶使用。然后,我們將考慮越來越多的高級主題,並簡要介紹 Java 字節碼和類文件格式。即使你快速跳過這以后的材料,也不要灰心!你可以通過使用 Byte Buddy 的標准 API 來完成任何操作,而無需了解任何 JVM 規范。要了解標准 API,只需繼續閱讀。

創建一個類

任何一個由 Byte Buddy 創建的類型都是通過 ByteBuddy 類的實例來完成的。通過簡單地調用 new ByteBuddy() 就可以創建一個新實例,然后就可以出發了。希望你使用一個集成開發環境,這樣在調用一個給定實例的方法時就能得到相應的提示。這樣,你的集成開發環境就會引導你完成相應的方法調用,防止手動在 Byte Buddy 文檔中查閱某個類的 API。正如之前所說,Byte Buddy 提供了一個領域特定語言,這樣就可以盡可能地提高人類可讀性。集成開發環境的提示在大部分情況下會指引你到正確的方向。說的夠多了,讓我們在 Java 編程環境中創建第一個類吧:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .make();

正如前面所設想的,上面的示例代碼會創建一個繼承至 Object 類型的類。這個動態創建的類型與直接擴展 Object 並且沒有實現任何方法、屬性和構造函數的類型是等價的。你可能已經注意到,我們都沒有命名動態生成的類型,通常在定義 Java 類時卻是必須的。當然,你也可以很容易地明確地命名這個類型:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .name("example.Type") .make();

如果沒有明確的命名會怎么樣呢?Byte Buddy 與 約定大於配置 息息相關,為你提供了我們認為比較方面的默認配置。至於類型命名,Byte Buddy 的默認配置提供了 NamingStrategy,它基於動態類型的超類名稱來隨機生成類名。此外,名稱定義在與父類相同的包下,這樣父類的包級訪問權限的方法對動態類型也可見。如果你將示例子類命名為 example.Foo,那么生成的名稱將會類似於 example.FooByteBuddy1376491271,這里的數字序列是隨機的。這個規則的例外情況就是當子類是從 java.lang 包下的類擴展時,就是 Object 所在的包。Java 的安全模型不允許自定義類型存放在這個命名空間下。因此,默認命名策略下,這些類型名稱將會冠以 net.bytebuddy.renamed 的前綴。

默認行為也許對你來說並不方便。感謝約定大於配置原則,你總是可以根據你的需要來選擇默認行為。這正是 ByteBuddy 的優越之處。通過 new ByteBuddy() 創建實例,你就創建了整套的默認配置。通過調用在這個配置上的方法,你就可以根據你的需要來訂制它。讓我們試試:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .with(new NamingStrategy.AbstractBase() { @Override public String subclass(TypeDescription superClass) { return "i.love.ByteBuddy." + superClass.getSimpleName(); } }) .subclass(Object.class) .make();

在上面這里例子中,我們創建了一個新的配置,在類型命名方面,不同於默認配置。匿名類被簡單實現為連接 i.love.ByteBuddy 和基類的簡要名稱。當擴展 Object 類型時,動態類將被命名為 i.love.ByteBuddy.Object。當創建自己的命名策略時,需要特別小心。Java 虛擬機就是使用名字來區分不同的類型的,這正是為什么要避免命名沖突的原因。如果你需要定制命名行為,請考慮使用 Byte Buddy 內置的 NamingStrategy.SuffixingRandom,你可以通過引入比默認對你應用更有意義的前綴來定制命名行為。

領域特定語言和不變性

在看過 Byte Buddy 這種領域特定語言的實際效果之后,我們需要簡要看一下這種語言的實現方式。有一個細節需要特別注意,這個語言是圍繞 不可變對象 構建的。事實上,Byte Buddy 中,幾乎所有的類都被構建成不可變的;極少數情況,我們不可能把對象構建成不可變的,我們會在該類的文檔中明確指出。如果你為 Byte Buddy 實現自定義功能,我們建議你遵守此原則。

作為所提到的不可變性的含義,例如配置 ByteBuddy 實例時,一定要小心。你也許會犯下面的錯誤:

ByteBuddy byteBuddy = new ByteBuddy(); byteBuddy.withNamingStrategy(new NamingStrategy.SuffixingRandom("suffix")); DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();

你或許希望使用 new NamingStrategy.SuffixingRandom("suffix") 來自定義動態類型的命名策略。不是修改存儲在 byteBuddy 變量中的實例,調用 withNamingStrategy 方法返回一個自定義的 ByteBuddy 實例,但是它卻直接被丟棄了。結果,還是使用原來創建的默認配置來創建動態類型。

重新定義或者重定基底已經存在的類

 

D瓜哥注

type rebasing 不知如何翻譯是好,暫且翻譯為“重定基底”。下文中,根據語句通順需要,偶爾也翻譯成“重定義”。如果有好的翻譯,歡迎給發PR。

到目前為止,我們僅僅演示了如何使用 Byte Buddy 來創建已知類的子類。相同的 API 還可用於增強已有類。增加已有類有兩種方式:

類型重定義(type redefinition)

當重定義一個類時,Byte Buddy 可以對一個已有的類添加屬性和方法,或者刪除已經存在的方法實現。如果使用其他的方法實現替換已經存在的方法實現,則原來存在的方法實現就會消失。例如,我們重定義下面這個類型

class Foo { String bar() { return "bar"; } }

從 bar 方法返回 "qux",那么該方法原來返回的 "bar" 等信息就會都被丟失掉。

類型重定基底(type rebasing)

當重定基底一個類時,Byte Buddy 保存基底類所有方法的實現。當 Byte Buddy 如執行類型重定義時,它將所有這些方法實現復制到具有兼容簽名的重新命名的私有方法中,而不是拋棄重寫的方法。這樣,就沒有實現會被丟失。重定義的方法可以繼續通過它們重命名過的名稱調用原來的方法。通過這種方式,上述 Foo 類就會被重定義成下面這個樣子:

class Foo { String bar() { return "foo" + bar$original(); } private String bar$original() { return "bar"; } }

原來返回 bar 的方法被保存到了另外一個方法里,因此還可以訪問。當重定基底一個類時,Byte Buddy 對待所有方法定義就像你定義一個子類一樣,例如,如果你想調用重定義方法的超類方法是,它會調用被重定義的方法。相反,它最終將這個假設的超級類別變成了上面顯示的重定義類型。

任何重定基底、重定義或子類都是使用相同的 API 來執行,接口由 DynamicType.Builder 來定義。這樣,可以將類定義為子類,然后更改代碼來替換重定類。你只需要修改 Byte Buddy 領域特定語言的一個單詞就能達到這個目的。這樣,在定義的未來階段,你就可以透明地切換任何一種方法:

new ByteBuddy().subclass(Foo.class) new ByteBuddy().redefine(Foo.class) new ByteBuddy().rebase(Foo.class)

這在本教程的其余部分都有所解釋。因為定義子類對於 Java 開發人員來說是如此地熟悉,所以,接下來的所有解釋以及 Byte Buddy 領域特定語言的實例都是用創建子類來演示。但是,請記住,所有類可以類似地通過重定義或重定基類來定義。

加載類

到目前為止,我們只定義並創建了一個動態類型,但是我們沒有使用它。由 Byte Buddy 創建的類型使用 DynamicType.Unloaded 的實例表示。顧名思義,這些類型不會加載到 Java 虛擬機中。相反,由 Byte Buddy 創建的類以二進制,Java 類文件格式形式表示。這樣,您可以決定要使用生成的類型做什么。例如,您可能希望從構建腳本運行 Byte Buddy,該腳本僅在部署之前生成 Java 類以增強應用程序。為此,DynamicType.Unloaded 類允許提取表示動態類型的字節數組。為方便起見,該類型還提供了一個 saveIn(File) 方法,可以將類存儲在給定的文件夾中。此外,它允許您使用 inject(File) 方法將類注入到現有的 Jar 文件中。

盡管直接訪問類的二進制形式簡單直接,但不幸的是,加載類型卻非常復雜。在 Java 中,所有的類都使用 ClassLoader 來加載。這種類加載器的一個例子是引導類加載器,它負責加載 Java 類庫中發布的類。另一方面,系統類加載器負責加載在 Java 應用程序的類路徑上的類。顯然,這些先前存在的類加載器都不知道我們創建的任何動態類。為了解決這個問題,我們必須找到能加載運行時生成類的其他可能性。 Byte Buddy 通過不同的方法提供解決方案:

  • 我們簡單地創建一個新的 ClassLoader,並明確地告知它一個特定動態創建的類的存在位置。因為 Java 類加載器是以層次結構組織的,所以我們將此類加載器定義為運行中的 Java 應用程序中已經存在的給定類加載器的子類。這樣,運行的Java程序的所有類型對於使用新的 ClassLoader 加載的動態類型都是可見的。

  • 通常,Java 類加載器在嘗試直接加載給定名稱的類型之前查詢其雙親 ClassLoader這意味着類加載器通常不會加載類型,以防其父類加載程序知道具有相同名稱的類型。為了這個目的,Byte Buddy提供了一個子類優先的類加載器的創建功能,它嘗試在查詢父類之前自己加載一個類型。除此之外,這種方法類似於上述方法。請注意,此方法不會覆蓋父類加載器的類型,而是影響此其他類型。

  • 最后,我們可以使用反射來將類型注入到現有的 ClassLoader 中。通常,類加載器被要求以其名稱提供給定類型。使用反射,我們可以圍繞這個原理,並調用一個protected方法,將類添加到類加載器中,而類加載器實際上並不知道如何定位這個動態類。

不幸的是,上面的方式有兩個缺點:

  • 如果我們創建一個新的 ClassLoader,這個類加載器就會定義一個新的命名空間。有意義的是,可以加載兩個具有相同名稱的類,只要這些類由兩個不同的類加載器加載即可。即使這兩個類代表相同的類實現,這兩個類也不會被 Java 虛擬機視為相等。這個等式的規則也適用於Java包。這意味着一個類 example.Foo 不能訪問另一個類 example.Bar 的包私有級的方法,如果兩個類不是由相同的類加載器加載的話。另外,如果 exam​​ple.Bar 擴展 example.Foo,任何覆蓋的包私有級的方法將變得不起作用,將會委托給原始實現。

  • 每當加載類時,一旦引用另一種類型的代碼段被解析,其類加載器就會查找該類中引用的任何類型。這個查找會委托給同一個類加載器。想象一下,我們動態創建兩個類 example.Foo 和 example.Bar。如果我們將 example.Foo 注入到一個現有的類加載器中,這個類加載器可能會嘗試找到 example.Bar。然而,這種查找會失敗,因為后一類是動態創建的,對於我們剛注入 example.Foo 類的類加載器是不可訪問的。因此,反射方法不能用於在類加載期間變得有效的循環依賴性的類。幸運的是,大多數 Java 虛擬機實現會在第一次主動使用時惰性地解析引用的類,這就是為什么類注入通常可以工作而沒有這些限制。另外在實踐中,由 Byte Buddy 創建的類通常不會受到這種循環的影響。

您可能會考慮到遇到循環依賴關系的可能性與您一次創建一個動態類型相關聯。但是,動態創建類型可能會觸發所謂的輔助類型的創建。這些類型由 Byte Buddy 自動創建,以提供對您正在創建的動態類型來訪問。我們在下一節中詳細了解輔助類型,現在不用擔心。但是,由於這個原因,我們建議您通過創建一個特定的 ClassLoader 來加載動態創建的類,而不是將它們注入現有類。

創建  DynamicType.Unloaded 之后,可以使用  ClassLoadingStrategy 加載此類型。 如果沒有提供這樣的策略,Byte Buddy 會根據提供的類加載器推測出這樣的策略,並為引導類加載器創建一個新的類加載器,其中不能使用反射注入類型,否則為默認值。Byte Buddy提供了幾種類加載策略,其中每種都遵循上述概念之一。這些策略定義在  ClassLoadingStrategy.Default 中,其中  WRAPPER 策略創建一個新的包裝  ClassLoaderCHILD_FIRST 策略創建一個類似於第一個子類優先的類加載器; INJECTION 策略使用反射注入動態類型。  WRAPPER 和  CHILD_FIRST 策略也可以在所謂的 清單版本中使用,即使在加載類之后,類型的二進制格式也被保留。這些替代版本使得類加載器的類的二進制表示可以通過 `ClassLoader

getResourceAsStream` 方法訪問。但是,請注意,這需要這些類加載器來維護對類的完整二進制表示的引用,這將占用 Java 虛擬機堆上的空間。因此,如果您打算實際訪問二進制格式,則應僅使用清單版本。由於 INJECTION 策略通過反射工作,並且無法更改 ClassLoader :: getResourceAsStream 方法的語義,因此它在清單版本中自然不可用。

我們來看看類加載的實際操作: 、

Class<?> type = new ByteBuddy() .subclass(Object.class) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded();

在上面的例子中,我們創建並加載了一個類。我們使用 WRAPPER 策略來加載適合大多數情況的類,就像我們之前提到的那樣。最后,getLoaded 方法返回一個 Java Class 的實例,它就表示現在加載的動態類。

請注意,加載類時,通過應用當前執行上下文的 ProtectionDomain 來執行預定義的類加載策略。或者,所有默認策略通過調用 withProtectionDomain 方法來提供明確保護域的規范。使用安全管理員(security manager)或使用已簽名的 Jar 中定義的類時,定義顯式保護域很重要。

重新加載類

class Foo { String m() { return "foo"; } } class Bar { String m() { return "bar"; } }
ByteBuddyAgent.install();
Foo foo = new Foo(); new ByteBuddy() .redefine(Bar.class) .name(Foo.class.getName()) .make() .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); assertThat(foo.m(), is("bar"));

操作沒有加載的類

package foo; class Bar { }
class MyApplication { public static void main(String[] args) { TypePool typePool = TypePool.Default.ofClassPath(); new ByteBuddy() .redefine(typePool.describe("foo.Bar").resolve(), // do not use 'Bar.class' ClassFileLocator.ForClassLoader.ofClassPath()) .defineField("qux", String.class) // we learn more about defining fields later .make() .load(ClassLoader.getSystemClassLoader()); assertThat(Bar.class.getDeclaredField("qux"), notNullValue()); } }

創建 Java Agents

class ToStringAgent { public static void premain(String arguments, Instrumentation instrumentation) { new AgentBuilder.Default() .type(isAnnotatedWith(ToString.class)) .transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader classloader) { return builder.method(named("toString")) .intercept(FixedValue.value("transformed")); } }).installOn(instrumentation); } }

在 Android 應用中加載類

使用泛型類

屬性和方法

String toString = new ByteBuddy() .subclass(Object.class) .name("example.Type") .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() // Java reflection API .toString();
String toString = new ByteBuddy() .subclass(Object.class) .name("example.Type") .method(named("toString")).intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .toString();
named("toString").and(returns(String.class)).and(takesArguments(0))
class Foo { public String bar() { return null; } public String foo() { return null; } public String foo(Object o) { return null; } } Foo dynamicFoo = new ByteBuddy() .subclass(Foo.class) .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!")) .method(named("foo")).intercept(FixedValue.value("Two!")) .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!")) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance();

深入細看一個固定值

new ByteBuddy() .subclass(Foo.class) .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0)) .make();

委托方法調用

class Source { public String hello(String name) { return null; } } class Target { public static String hello(String name) { return "Hello " + name + "!"; } } String helloWorld = new ByteBuddy() .subclass(Source.class) .method(named("hello")).intercept(MethodDelegation.to(Target.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .hello("World");
class Target { public static String intercept(String name) { return "Hello " + name + "!"; } public static String intercept(int i) { return Integer.toString(i); } public static String intercept(Object o) { return o.toString(); } }
void foo(Object o1, Object o2)
void foo(@Argument(0) Object o1, @Argument(1) Object o2)
class MemoryDatabase { public List<String> load(String info) { return Arrays.asList(info + ": foo", info + ": bar"); } } class LoggerInterceptor { public static List<String> log(@SuperCall Callable<List<String>> zuper) throws Exception { System.out.println("Calling database"); try { return zuper.call(); } finally { System.out.println("Returned from database"); } } } MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance();
class LoggingMemoryDatabase extends MemoryDatabase { private class LoadMethodSuperCall implements Callable { private final String info; private LoadMethodSuperCall(String info) { this.info = info; } @Override public Object call() throws Exception { return LoggingMemoryDatabase.super.load(info); } } @Override public List<String> load(String info) { return LoggerInterceptor.log(new LoadMethodSuperCall(info)); } }
class ChangingLoggerInterceptor { public static List<String> log(String info, @Super MemoryDatabase zuper) { System.out.println("Calling database"); try { return zuper.load(info + " (logged access)"); } finally { System.out.println("Returned from database"); } } }
class Loop { public String loop(String value) { return value; } public int loop(int value) { return value; } }
class Interceptor { @RuntimeType public static Object intercept(@RuntimeType Object value) { System.out.println("Invoked method with: " + value); return value; } }
interface Forwarder<T, S> { T to(S target); }
class ForwardingLoggerInterceptor { private final MemoryDatabase memoryDatabase; // constructor omitted public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) { System.out.println("Calling database"); try { return pipe.to(memoryDatabase); } finally { System.out.println("Returned from database"); } } } MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation.withDefaultConfiguration() .withBinders(Pipe.Binder.install(Forwarder.class))) .to(new ForwardingLoggerInterceptor(new MemoryDatabase())) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance();

調用超類方法

new ByteBuddy() .subclass(Object.class) .make()
new ByteBuddy() .subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_TYPE) .make()

調用默認方法

interface First { default String qux() { return "FOO"; } } interface Second { default String qux() { return "BAR"; } }
new ByteBuddy(ClassFileVersion.JAVA_V8) .subclass(Object.class) .implement(First.class) .implement(Second.class) .method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class)) .make()

調用特定方法

public class SampleClass { public SampleClass(int unusedValue) { super(); } }
new ByteBuddy() .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS) .defineConstructor(Arrays.<Class<?>>asList(int.class), Visibility.PUBLIC) .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())) .make()

訪問屬性

class UserType { public String doSomething() { return null; } } interface Interceptor { String doSomethingElse(); } interface InterceptionAccessor { Interceptor getInterceptor(); void setInterceptor(Interceptor interceptor); } interface InstanceCreator { Object makeInstance(); }
Class<? extends UserType> dynamicUserType = new ByteBuddy() .subclass(UserType.class) .method(not(isDeclaredBy(Object.class))) .intercept(MethodDelegation.toField("interceptor")) .defineField("interceptor", Interceptor.class, Visibility.PRIVATE) .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty()) .make() .load(getClass().getClassLoader()) .getLoaded();
InstanceCreator factory = new ByteBuddy() .subclass(InstanceCreator.class) .method(not(isDeclaredBy(Object.class))) .intercept(MethodDelegation.construct(dynamicUserType)) .make() .load(dynamicUserType.getClassLoader()) .getLoaded().newInstance();
class HelloWorldInterceptor implements Interceptor { @Override public String doSomethingElse() { return "Hello World!"; } } UserType userType = (UserType) factory.makeInstance(); ((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());

雜項

注解

@Retention(RetentionPolicy.RUNTIME) @interface RuntimeDefinition { } class RuntimeDefinitionImpl implements RuntimeDefinition { @Override public Class<? extends Annotation> annotationType() { return RuntimeDefinition.class; } } new ByteBuddy() .subclass(Object.class) .annotateType(new RuntimeDefinitionImpl()) .make();
new ByteBuddy() .subclass(Object.class) .annotateType(new RuntimeDefinitionImpl()) .method(named("toString")) .intercept(SuperMethodCall.INSTANCE) .annotateMethod(new RuntimeDefinitionImpl()) .defineField("foo", Object.class) .annotateField(new RuntimeDefinitionImpl())

類型注解

Byte Buddy 暴露並編寫了類型注解,它們被引入到 Java 8,並成為其中的一部分。

屬性附加器

class AnnotatedMethod { @SomeAnnotation void bar() { } } new ByteBuddy() .subclass(AnnotatedMethod.class) .method(named("bar")) .intercept(StubMethod.INSTANCE) .attribute(MethodAttributeAppender.ForInstrumentedMethod.INSTANCE)

定制化儀表

LDC     10 // stack contains 10 LDC 50 // stack contains 10, 50 IADD // stack contains 60 IRETURN // stack is empty
12 00 01 12 00 02 60 AC
enum IntegerSum implements StackManipulation { INSTANCE; // singleton @Override public boolean isValid() { return true; } @Override public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) { methodVisitor.visitInsn(Opcodes.IADD); return new Size(-1, 0); } }
enum SumMethod implements ByteCodeAppender { INSTANCE; // singleton @Override public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) { if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) { throw new IllegalArgumentException(instrumentedMethod + " must return int"); } StackManipulation.Size operandStackSize = new StackManipulation.Compound( IntegerConstant.forValue(10), IntegerConstant.forValue(50), IntegerSum.INSTANCE, MethodReturn.INTEGER ).apply(methodVisitor, implementationContext); return new Size(operandStackSize.getMaximalSize(), instrumentedMethod.getStackSize()); } }
enum SumImplementation implements Implementation { INSTANCE; // singleton @Override public InstrumentedType prepare(InstrumentedType instrumentedType) { return instrumentedType; } @Override public ByteCodeAppender appender(Target implementationTarget) { return SumMethod.INSTANCE; } }
abstract class SumExample { public abstract int calculate(); } new ByteBuddy() .subclass(SumExample.class) .method(named("calculate")) .intercept(SumImplementation.INSTANCE) .make()

創建自定義分配器

enum ToStringAssigner implements Assigner { INSTANCE; // singleton @Override public StackManipulation assign(TypeDescription.Generic source, TypeDescription.Generic target, Assigner.Typing typing) { if (!source.isPrimitive() && target.represents(String.class)) { MethodDescription toStringMethod = new TypeDescription.ForLoadedType(Object.class) .getDeclaredMethods() .filter(named("toString")) .getOnly(); return MethodInvocation.invoke(toStringMethod).virtual(sourceType); } else { return StackManipulation.Illegal.INSTANCE; } } }
new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(FixedValue.value(42) .withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE), Assigner.Typing.STATIC)) .make()

創建自定義參數綁定器

@Retention(RetentionPolicy.RUNTIME) @interface StringValue { String value(); }
enum StringValueBinder implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> { INSTANCE; // singleton @Override public Class<StringValue> getHandledType() { return StringValue.class; } @Override public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loaded<StringValue> annotation, MethodDescription source, ParameterDescription target, Implementation.Target implementationTarget, Assigner assigner, Assigner.Typing typing) { if (!target.getType().asErasure().represents(String.class)) { throw new IllegalStateException(target + " makes illegal use of @StringValue"); } StackManipulation constant = new TextConstant(annotation.loadSilent().value()); return new MethodDelegationBinder.ParameterBinding.Anonymous(constant); } }
class ToStringInterceptor { public static String makeString(@StringValue("Hello!") String value) { return value; } } new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(MethodDelegation.withDefaultConfiguration() .withBinders(StringValueBinder.INSTANCE) .to(ToStringInterceptor.class)) .make()

附錄 A: 版權聲明

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM