使用 Javassist 進行類轉換

Mondo 科技 更新 2024-02-01

使用 j**assist 將方法轉換為位元組碼。

現在我們已經介紹了使用反射的 J**a 格式和執行時訪問,現在是本系列進入更高階主題的時候了。 本月,我將開始本系列的第二部分,其中 J**A 資訊只是應用程式操作的另一種形式的資料結構。 我會把這個話題的整個事情稱為課堂工作。

我將開始討論使用 j**assist 位元組碼操作庫的課堂工作。 J**Assist 不僅是乙個位元組碼處理庫,而且它還具有另乙個功能,使其成為嘗試類工作的絕佳起點。 使用 j**assist 更改 j**a 類的位元組碼的功能不需要您真正了解位元組碼或 j**a 虛擬機器 jvm 的任何資訊。 這個功能在某些方面有利有弊——我通常不主張使用你不了解的技術——但它確實使位元組碼操作比在單個指令級別工作的框架更可行。

J**Assist 允許您檢查、編輯和建立 J**A 二進位類。 檢查方面與通過 Reflection API 直接在 J**a 中基本相同,但是當您想要修改類而不僅僅是執行它們時,另一種訪問此資訊的方法很有用。 這是因為 JVM 的設計目的不是為了在類載入到 JVM 中後提供任何訪問原始類資料的方法,這需要在 JVM 之外完成。

J**Assist 使用j**assist.classpool類跟蹤和控制它們所操作的類。 該類的工作方式與 JVM 類裝入器非常相似,但有乙個重要的區別是,類池不是將已載入的可執行類作為應用程式的一部分鏈結,而是通過 J**Assist API 使已載入的類作為資料可用。 您可以使用從 JVM 搜尋路徑掛載的預設類池,也可以定義乙個類池來搜尋您自己的路徑列表。 您甚至可以直接從位元組陣列或流載入二進位類,並從頭開始建立新類。

裝載到類池中的類由j**assist.ctclass例項表示形式。 使用標準 J**Aj**a.lang.class類相同ctclass提供檢查類資料(如字段和方法)的方法。 不過,這只是ctclass它還定義了向類新增新字段、方法和建構函式以及更改類、父類和介面的方法。 奇怪的是,J**Assist 沒有提供任何刪除類中的字段、方法或建構函式的方法。

字段、方法和建構函式由j**assist.ctfield、 j**assist.ctmethodj**assist.ctconstructor的例項表示形式。 這些類定義用於修改它們所表示的物件的所有方法的方法,包括方法或建構函式中的實際位元組碼內容。

J**Assist 允許您完全替換方法或建構函式的位元組碼主體,或者選擇在現有主體的開頭或結尾新增位元組碼(以及建構函式中的一些其他變數)。 無論哪種情況,新位元組碼都會被宣告為類 j**a 的源string塊被傳入 。 j**assist 方法有效地將您提供的源編譯為 j**a 位元組碼,然後將它們插入到目標方法或建構函式的主體中。

J**Assist 接受的源與 J**a 的源並不完全相同,但主要區別在於新增了一些特殊的識別符號來表示方法或構造函式引數、方法返回值以及插入的 ** 中可能使用的其他內容。 這些特殊識別符號由符號表示所以他們不會干擾**中的其他任何事情。

在傳遞給 jassist 的原始碼中可以執行的操作存在一些限制。 第乙個限制是使用的格式,它必須是單個語句或塊。 在大多數情況下,這不是乙個限制,因為您可以將所需的任何語句序列放入乙個塊中。 下面是乙個示例,說明如何使用特殊的 j**assist 識別符號來表示方法中的前兩個引數:

對源的更實質性限制是,您不能引用在新增或塊外宣告中宣告的區域性變數。 這意味著,如果在方法的開頭和結尾新增方法,則通常無法將資訊從開頭新增的內容傳遞到末尾新增的資訊。 可以繞過此限制,但它很複雜 - 通常需要嘗試將單獨插入的 ** 合併到單個塊中。

作為使用 J**Assist 的示例,我將使用乙個通常直接在原始碼中處理的任務:測量執行方法所需的時間。 這可以通過在方法開始時記錄當前時間,然後在方法結束時再次檢查當前時間並計算兩個值之間的差值,在源中輕鬆完成。 如果沒有來源**,獲取此時間資訊將更加困難。 這就是課堂作業派上用場的地方——它允許您對任何方法進行此更改,而無需主動進行更改。

清單 1 顯示了乙個(糟糕的)示例方法,我將其用作定時試驗的實驗:stringbuilderbuildstring方法。 此方法使用的方法,所有 j**a 效能優化專家都會告訴您不要使用這種方法來構造具有任意長度的構造string— 它通過在字串末尾重複附加單個字元來生成更長的字串。 由於字串是不可變的,因此這種方法意味著每個新字串都是通過迴圈構造的:使用從舊字串複製的資料並在末尾新增新字元。 最終結果是,當您使用此方法生成更長的字串時,它變得越來越昂貴。

清單 1需要計時的方法。

public class stringbuilder return result; }public static void main(string ar**)
因為此方法有乙個源,所以我將向您展示如何直接新增計時資訊。 在使用 j**assist 時,它也可以用作模型。 清單 2 僅顯示buildstring()方法,向其新增定時函式。 這裡沒有太大變化。 新增的 ** 只是將開始時間儲存為區域性變數,然後在方法結束時計算持續時間並將其列印到控制台。

清單 2計時方法。

private string buildstring(int length) system.out.println("call to buildstring took " + system.currenttimemillis()-start) +" ms."); return result; }
使用 j**assist 操作位元組碼應該不難。 j**assist 提供了一種在方法的開頭和結尾新增 ** 的方法,別忘了,這正是我通過向方法新增時序資訊所做的。

儘管如此,還是存在障礙。 在描述 J**Assist 如何允許您新增 ** 時,我提到 ** 新增的 ** 不能引用方法中其他地方定義的區域性變數。 這個限制使我無法使用與原始碼中使用的相同的方法在 jassist 中實現 time,在這種情況下,我在開頭的加法中定義乙個新的區域性變數,並在末尾的加法中引用它。

那麼有沒有其他方法可以達到同樣的效果呢? 是的,我可以向類新增乙個新的成員字段,並使用此字段而不是區域性變數。 不過,這是乙個糟糕的解決方案,在一般使用中有一些侷限性。 例如,考慮遞迴方法會發生什麼。 每次方法呼叫自身時,上次儲存的開始時間值都會被覆蓋並丟失。

幸運的是,有乙個更簡潔的解決方案。 我可以保持原始方法不變,只需更改方法名稱,然後新增乙個具有原始方法名稱的新方法。 攔截器方法可以使用與原始方法相同的簽名,包括返回相同的值。 清單 3 顯示了以這種方式調整原始碼後的樣子:

清單 3將方法新增到源。

private string buildstring$impl(int length) return result; }private string buildstring(int length)
J**Assist可以很好地利用這種使用***方法的方法。 因為整個方法是乙個塊,所以我可以在正文中定義和使用區域性變數,而不會出現任何問題。 為方法生成源也很容易——對於任何可能的方法,只需要一些替換。

若要實現加法方法的計時,需要使用 J**Assist 基礎知識中描述的一些 J**Assist API。 清單 4 顯示了 **,這是乙個帶有兩個命令列引數的應用程式,它們提供了要計時的類名和方法名。 main()方法的主體只是提供類資訊,然後將其傳遞給addtiming()方法處理實際修改。 addtiming()該方法首先通過將 “ 附加到 name 來完成$impl”重新命名現有方法,然後使用原始方法名稱建立該方法的副本。 然後,它將 copy 方法的正文替換為乙個計時器,該計時器包含對重新命名的原始方法的呼叫。 清單 4使用 j**assist 新增 *** 方法。

public class jassisttiming else }catch (cannotcompileexception ex) catch (notfoundexception ex) catch (ioexception ex) else }private static void addtiming(ctclass clas, string mname) throws notfoundexception, cannotcompileexception body.append(nname + "($$n"); // finish body text generation with call to print the timing // information, and return s**ed value (if not void) body.append("system.out.println(\"call to method " + mname + " took \" + system.currenttimemillis()-start) +" + "\" ms.\");"); if (!"void".equals(type)) body.append("}"); // replace the body of the interceptor method with generated // code block and add it to class mnew.setbody(body.tostring())clas.addmethod(mnew); // print the generated code block just to show what was done system.out.println("interceptor method body:"); system.out.println(body.tostring())
在構造 *** 方法的主體時使用乙個j**a.lang.stringbuffer以累積正文文字(這顯示了處理過程。string正確的施工方式,用stringbuilder施工中使用的方法是相對的)。此更改取決於原始方法是否具有返回值。 如果它有乙個返回值,那麼構造的 ** 會將這個值儲存在乙個區域性變數中,以便它可以在 *** 方法的末尾返回。 如果原始方法型別為void,則不需要儲存任何內容,並且不需要在 *** 方法中返回任何內容。

除了對原始(重新命名)方法的呼叫之外,實際的正文內容看起來就像標準的 j**a 在**中一樣body.append(nname + "($$n")這條線,其中nname它是原始方法的修改方法的名稱。 在通話中使用識別符號是 j**assist 如何表示正在構造的方法的一系列引數。 通過在對原始方法的呼叫中使用此識別符號,可以將呼叫 *** 方法時提供的引數傳遞給原始方法。

清單 5 顯示了如何首先執行未修改的stringbuilder程式,然後執行jassisttiming程式新增時序資訊,最後執行修改後的stringbuilder程式的結果。 您可以看到修改後的那個stringbuilder執行時報告執行時間,您還可以看到,由於字串構造效率低下而增加的時間**比由於構造的字串長度增加而增加的時間要快得多。

清單 5執行此應用。

[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000constructed string of length 1000constructed string of length 2000constructed string of length 4000constructed string of length 8000constructed string of length 16000[dennis]$ j**a -cp j**assist.jar:. jassisttiming stringbuilder buildstringinterceptor method body:added timing to method stringbuilder.buildstring[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000call to method buildstring took 37 ms.constructed string of length 1000call to method buildstring took 59 ms.constructed string of length 2000call to method buildstring took 181 ms.constructed string of length 4000call to method buildstring took 863 ms.constructed string of length 8000call to method buildstring took 4154 ms.constructed string of length 16000
J**Assist 允許您處理原始碼而不是實際的位元組碼指令清單,從而使課堂工作變得容易。 但這種便利性也有缺點。 正如我在所有位元組碼的原始碼中提到的,J**Assist 使用的原始碼與 J**a 語言並不完全相同。 除了識別 j**a 中的特殊識別符號外,j**assist 還實現了比 j**a 語言規範要求的更寬鬆的編譯時檢查。 所以,如果你不小心,你會從原始碼生成位元組碼,可以產生令人驚訝的結果。 例如,清單 6 顯示了方法開頭使用的區域性變數的型別long成為int次。 J**Assist 將獲取原始碼並將其轉換為有效的位元組碼,但它獲得的時間毫無意義。 如果嘗試直接在 j**a 程式中編譯此賦值,則會出現編譯錯誤,因為它違反了 j**a 語言的規則:縮小賦值範圍需要型別覆蓋。

清單 6將是乙個long儲存到乙個int中間。

[dennis]$ j**a -cp j**assist.jar:. jassisttiming stringbuilder buildstringinterceptor method body:added timing to method stringbuilder.buildstring[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000call to method buildstring took 1060856922184 ms.constructed string of length 1000call to method buildstring took 1060856922172 ms.constructed string of length 2000call to method buildstring took 1060856922382 ms.constructed string of length 4000call to method buildstring took 1060856922809 ms.constructed string of length 8000call to method buildstring took 1060856926253 ms.constructed string of length 16000
根據原始碼中的內容,您甚至可能讓 j**assist 生成無效的位元組碼。 清單 7 顯示了乙個這樣的示例,我將在此示例中:jassisttiming**已修改為始終將計時方法視為返回乙個int價值。 j**assist 也會毫無問題地接受這個源,但是當我嘗試執行生成的位元組碼時,它無法驗證。

清單 7將是乙個string儲存到乙個int中間。

[dennis]$ j**a -cp j**assist.jar:. jassisttiming stringbuilder buildstringinterceptor method body:added timing to method stringbuilder.buildstring[dennis]$ j**a stringbuilder 1000 2000 4000 8000 16000exception in thread "main" j**a.lang.verifyerror:(class: stringbuilder, method: buildstring signature:(i)lj**a/lang/string;) expecting to find integer on stack
只要您小心提供給 j**assist 的源,這不是問題。 但是,重要的是要認識到 j**assist 不會捕獲 ** 中的所有錯誤,因此您可能會遇到不可預見的錯誤結果。

J**Assist 比我們在本文中討論的要廣泛得多。 下個月,我們將仔細研究 J**Assist 提供的一些特殊功能,用於批量修改類,以及在執行時載入類時動態修改類。 這些功能使 J**Assist 成為在您的應用程式中實現的絕佳工具,因此請務必繼續關注我們,了解這個強大的工具的全部內容。

相關問題答案

    當樣式轉換正在進行時

    當你對 沒有希望時,往往 可能已經開始了,大大小小的板塊轉換,而且款式正在進行中。大牌已經連續三天強勢,跌了三年的重量級股終於在年底開始出現上漲跡象,風格能否轉為藍籌股還有待觀察,但至少這三天 股強於小盤股。沒有人能踩著市場的所有節奏,買乙個小盤口,至少這三年不難過,所以,有短期的不適要忍受,畢竟賺...

    如何用“切入核心”造句?

    絕對 是乙個成語,描述了果斷 果斷 毫不猶豫和堅定不移的含義。這個成語起源於 孟子 全心全意 切釘,不顧困難 形容切鐵釘的果斷,不顧事情的艱辛和艱辛。在現代用法中,它常被用來描述人們做事的態度和風格,表達堅決果斷的決定和行動。使用示例 他對自己的工作非常投入,目標明確,在推動專案發展方面具有決定性作...

    如何用“ambition”造句?

    野心 一詞是指乙個人對未來渴望和追求的目標 願望或抱負。野心通常與個人的願望 追求 職業或成就等有關。例如 小明有志於成為一名科學家,他想為世界做出重大貢獻。.她的志向是成為一名成功的企業家,並在商界取得重大成功。.這個團隊致力於環境保護的雄心壯志,他們希望創造乙個更可持續的未來。有抱負 的常見用法...

    “there are boards and eyes”如何造句?

    有板子和眼睛 是形容乙個有條理 有條不紊 有條理的人。這個成語通常用於描述未定義 非常有紀律 沒有隨意或隨意的行為。它也可以用來描述事物的排列或組織。這個成語以多種方式用於描述乙個人或一件事,通常用於讚美或描述某人的能力 經驗或組織。例如 .他在工作中總是很清醒,從不驚慌失措,而且計畫周密。.她把這...

    如何使用 AI 將 PDF 轉換為 Excel?3 種方式

    Excel作為日常辦公文件格式之一,廣泛應用於財務 審計 採購等工作中。我們經常需要將Excel轉換為PDF格式以確保文件的安全性,但是當我們必須再次編輯內容時,可能會很困難。傳統方法是建立乙個新的 Excel 文件並手動將資料輸入到窗格中,但是如果您需要處理大量資料內容怎麼辦?今天,我想和大家分享...