Java 编程的动态性, 第4部分: 用 Javassist 进行类转换--转载
講過了 Java 類格式和利用反射進(jìn)行的運(yùn)行時(shí)訪問后,本系列到了進(jìn)入更高級(jí)主題的時(shí)候了。本月我將開始本系列的第二部分,在這里 Java 類信息只不過是由應(yīng)用程序操縱的另一種形式的數(shù)據(jù)結(jié)構(gòu)而已。我將這個(gè)主題的整個(gè)內(nèi)容稱為?classworking。
我將以 Javassist 字節(jié)碼操作庫作為對(duì) classworking 的討論的開始。Javassist 不僅是一個(gè)處理字節(jié)碼的庫,而且更因?yàn)樗牧硪豁?xiàng)功能使得它成為試驗(yàn) classworking 的很好的起點(diǎn)。這一項(xiàng)功能就是:可以用 Javassist 改變 Java 類的字節(jié)碼,而無需真正了解關(guān)于字節(jié)碼或者 Java 虛擬機(jī)(Java virtual machine JVM)結(jié)構(gòu)的任何內(nèi)容。從某方面將這一功能有好處也有壞處 -- 我一般不提倡隨便使用不了解的技術(shù) -- 但是比起在單條指令水平上工作的框架,它確實(shí)使字節(jié)碼操作更可具有可行性了。
Javassist 基礎(chǔ)
Javassist 使您可以檢查、編輯以及創(chuàng)建 Java 二進(jìn)制類。檢查方面基本上與通過 Reflection API 直接在 Java 中進(jìn)行的一樣,但是當(dāng)想要修改類而不只是執(zhí)行它們時(shí),則另一種訪問這些信息的方法就很有用了。這是因?yàn)?JVM 設(shè)計(jì)上并沒有提供在類裝載到 JVM 中后訪問原始類數(shù)據(jù)的任何方法,這項(xiàng)工作需要在 JVM 之外完成。
Javassist 使用?javassist.ClassPool?類跟蹤和控制所操作的類。這個(gè)類的工作方式與 JVM 類裝載器非常相似,但是有一個(gè)重要的區(qū)別是它不是將裝載的、要執(zhí)行的類作為應(yīng)用程序的一部分鏈接,類池使所裝載的類可以通過 Javassist API 作為數(shù)據(jù)使用。可以使用默認(rèn)的類池,它是從 JVM 搜索路徑中裝載的,也可以定義一個(gè)搜索您自己的路徑列表的類池。甚至可以直接從字節(jié)數(shù)組或者流中裝載二進(jìn)制類,以及從頭開始創(chuàng)建新類。
裝載到類池中的類由?javassist.CtClass?實(shí)例表示。與標(biāo)準(zhǔn)的 Java?java.lang.Class?類一樣,?CtClass?提供了檢查類數(shù)據(jù)(如字段和方法)的方法。不過,這只是?CtClass?的部分內(nèi)容,它還定義了在類中添加新字段、方法和構(gòu)造函數(shù)、以及改變類、父類和接口的方法。奇怪的是,Javassist 沒有提供刪除一個(gè)類中字段、方法或者構(gòu)造函數(shù)的任何方法。
字段、方法和構(gòu)造函數(shù)分別由?javassist.CtField、javassist.CtMethod?和?javassist.CtConstructor?的實(shí)例表示。這些類定義了修改由它們所表示的對(duì)象的所有方法的方法,包括方法或者構(gòu)造函數(shù)中的實(shí)際字節(jié)碼內(nèi)容。
所有字節(jié)碼的源代碼?
Javassist 讓您可以完全替換一個(gè)方法或者構(gòu)造函數(shù)的字節(jié)碼正文,或者在現(xiàn)有正文的開始或者結(jié)束位置選擇性地添加字節(jié)碼(以及在構(gòu)造函數(shù)中添加其他一些變量)。不管是哪種情況,新的字節(jié)碼都作為類 Java 的源代碼聲明或者?String?中的塊傳遞。Javassist 方法將您提供的源代碼高效地編譯為 Java 字節(jié)碼,然后將它們插入到目標(biāo)方法或者構(gòu)造函數(shù)的正文中。
Javassist 接受的源代碼與 Java 語言的并不完全一致,不過主要的區(qū)別只是增加了一些特殊的標(biāo)識(shí)符,用于表示方法或者構(gòu)造函數(shù)參數(shù)、方法返回值和其他在插入的代碼中可能用到的內(nèi)容。這些特殊標(biāo)識(shí)符以符號(hào)?$?開頭,所以它們不會(huì)干擾代碼中的其他內(nèi)容。
對(duì)于在傳遞給 Javassist 的源代碼中可以做的事情有一些限制。第一項(xiàng)限制是使用的格式,它必須是單條語句或者塊。在大多數(shù)情況下這算不上是限制,因?yàn)榭梢詫⑺枰娜魏握Z句序列放到塊中。下面是一個(gè)使用特殊 Javassist 標(biāo)識(shí)符表示方法中前兩個(gè)參數(shù)的例子,這個(gè)例子用來展示其使用方法:
{System.out.println("Argument 1: " + $1);System.out.println("Argument 2: " + $2); }對(duì)于源代碼的一項(xiàng)更實(shí)質(zhì)性的限制是不能引用在所添加的聲明或者塊外聲明的局部變量。這意味著如果在方法開始和結(jié)尾處都添加了代碼,那么一般不能將在開始處添加的代碼中的信息傳遞給在結(jié)尾處添加的代碼。有可能繞過這項(xiàng)限制,但是繞過是很復(fù)雜的 -- 通常需要設(shè)法將分別插入的代碼合并為一個(gè)塊。
回頁首
用 Javassist 進(jìn)行 Classworking
作為使用 Javassist 的一個(gè)例子,我將使用一個(gè)通常直接在源代碼中處理的任務(wù):測(cè)量執(zhí)行一個(gè)方法所花費(fèi)的時(shí)間。這在源代碼中可以容易地完成,只要在方法開始時(shí)記錄當(dāng)前時(shí)間、之后在方法結(jié)束時(shí)再次檢查當(dāng)前時(shí)間并計(jì)算兩個(gè)值的差。如果沒有源代碼,那么得到這種計(jì)時(shí)信息就要困難得多。這就是 classworking 方便的地方 -- 它讓您對(duì)任何方法都可以作這種改變,并且不需要有源代碼。
清單 1 顯示了一個(gè)(不好的)示例方法,我用它作為我的計(jì)時(shí)試驗(yàn)的實(shí)驗(yàn)品:?StringBuilder?類的?buildString?方法。這個(gè)方法使用一種所有 Java 性能優(yōu)化的高手都會(huì)叫您?不要使用的方法構(gòu)造一個(gè)具有任意長度的?String?-- 它通過反復(fù)向字符串的結(jié)尾附加單個(gè)字符來產(chǎn)生更長的字符串。因?yàn)樽址遣豢勺兊?#xff0c;所以這種方法意味著每次新的字符串都要通過一個(gè)循環(huán)來構(gòu)造:使用從老的字符串中拷貝的數(shù)據(jù)并在結(jié)尾添加新的字符。最終的效果是用這個(gè)方法產(chǎn)生更長的字符串時(shí),它的開銷越來越大。
清單 1. 需要計(jì)時(shí)的方法
public class StringBuilder {private String buildString(int length) {String result = "";for (int i = 0; i < length; i++) {result += (char)(i%26 + 'a');}return result;}public static void main(String[] argv) {StringBuilder inst = new StringBuilder();for (int i = 0; i < argv.length; i++) {String result = inst.buildString(Integer.parseInt(argv[i]));System.out.println("Constructed string of length " +result.length());}} }添加方法計(jì)時(shí)
因?yàn)橛羞@個(gè)方法的源代碼,所以我將為您展示如何直接添加計(jì)時(shí)信息。它也作為使用 Javassist 時(shí)的一個(gè)模型。清單 2 只展示了buildString()?方法,其中添加了計(jì)時(shí)功能。這里沒有多少變化。添加的代碼只是將開始時(shí)間保存為局部變量,然后在方法結(jié)束時(shí)計(jì)算持續(xù)時(shí)間并打印到控制臺(tái)。
清單 2. 帶有計(jì)時(shí)的方法
private String buildString(int length) {long start = System.currentTimeMillis();String result = "";for (int i = 0; i < length; i++) {result += (char)(i%26 + 'a');}System.out.println("Call to buildString took " +(System.currentTimeMillis()-start) + " ms.");return result;}用 Javassist 來做
來做 使用 Javassist 操作類字節(jié)碼以得到同樣的效果看起來應(yīng)該不難。Javassist 提供了在方法的開始和結(jié)束位置添加代碼的方法,別忘了,我在為該方法中加入計(jì)時(shí)信息就是這么做的。
不過,還是有障礙。在描述 Javassist 是如何讓您添加代碼時(shí),我提到添加的代碼不能引用在方法中其他地方定義的局部變量。這種限制使我不能在 Javassist 中使用在源代碼中使用的同樣方法實(shí)現(xiàn)計(jì)時(shí)代碼,在這種情況下,我在開始時(shí)添加的代碼中定義了一個(gè)新的局部變量,并在結(jié)束處添加的代碼中引用這個(gè)變量。
那么還有其他方法可以得到同樣的效果嗎?是的,我?可以在類中添加一個(gè)新的成員字段,并使用這個(gè)字段而不是局部變量。不過,這是一種糟糕的解決方案,在一般性的使用中有一些限制。例如,考慮在一個(gè)遞歸方法中會(huì)發(fā)生的事情。每次方法調(diào)用自身時(shí),上次保存的開始時(shí)間值就會(huì)被覆蓋并且丟失。
幸運(yùn)的是有一種更簡潔的解決方案。我可以保持原來方法的代碼不變,只改變方法名,然后用原來的方法名增加一個(gè)新方法。這個(gè)?攔截器(interceptor)方法可以使用與原來方法同樣的簽名,包括返回同樣的值。清單 3 展示了通過這種方法改編后源代碼看上去的樣子:
清單 3. 在源代碼中添加一個(gè)攔截器方法
private String buildString$impl(int length) {String result = "";for (int i = 0; i < length; i++) {result += (char)(i%26 + 'a');}return result;}private String buildString(int length) {long start = System.currentTimeMillis();String result = buildString$impl(length);System.out.println("Call to buildString took " +(System.currentTimeMillis()-start) + " ms.");return result;}通過 Javassist 可以很好地利用這種使用攔截器方法的方法。因?yàn)檎麄€(gè)方法是一個(gè)塊,所以我可以毫無問題地在正文中定義并且使用局部變量。為攔截器方法生成源代碼也很容易 -- 對(duì)于任何可能的方法,只需要幾個(gè)替換。
運(yùn)行攔截
實(shí)現(xiàn)添加方法計(jì)時(shí)的代碼要用到在?Javassist 基礎(chǔ)中描述的一些 Javassist API。清單 4 展示了該代碼,它是一個(gè)帶有兩個(gè)命令行參數(shù)的應(yīng)用程序,這兩個(gè)參數(shù)分別給出類名和要計(jì)時(shí)的方法名。?main()?方法的正文只給出類信息,然后將它傳遞給?addTiming()?方法以處理實(shí)際的修改。addTiming()?方法首先通過在名字后面附加“?$impl”?重命名現(xiàn)有的方法,接著用原來的方法名創(chuàng)建該方法的一個(gè)拷貝。然后它用含有對(duì)經(jīng)過重命名的原方法的調(diào)用的計(jì)時(shí)代碼替換拷貝方法的正文。
清單4. 用 Javassist 添加攔截器方法
public class JassistTiming {public static void main(String[] argv) {if (argv.length == 2) {try {// start by getting the class file and methodCtClass clas = ClassPool.getDefault().get(argv[0]);if (clas == null) {System.err.println("Class " + argv[0] + " not found");} else {// add timing interceptor to the classaddTiming(clas, argv[1]);clas.writeFile();System.out.println("Added timing to method " +argv[0] + "." + argv[1]);}} catch (CannotCompileException ex) {ex.printStackTrace();} catch (NotFoundException ex) {ex.printStackTrace();} catch (IOException ex) {ex.printStackTrace();}} else {System.out.println("Usage: JassistTiming class method-name");}}private static void addTiming(CtClass clas, String mname)throws NotFoundException, CannotCompileException {// get the method information (throws exception if method with// given name is not declared directly by this class, returns// arbitrary choice if more than one with the given name)CtMethod mold = clas.getDeclaredMethod(mname);// rename old method to synthetic name, then duplicate the// method with original name for use as interceptorString nname = mname+"$impl";mold.setName(nname);CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null);// start the body text generation by saving the start time// to a local variable, then call the timed method; the// actual code generated needs to depend on whether the// timed method returns a valueString type = mold.getReturnType().getName();StringBuffer body = new StringBuffer();body.append("{\nlong start = System.currentTimeMillis();\n");if (!"void".equals(type)) {body.append(type + " result = ");}body.append(nname + "($$);\n");// finish body text generation with call to print the timing// information, and return saved value (if not void)body.append("System.out.println(\"Call to method " + mname +" took \" +\n (System.currentTimeMillis()-start) + " +"\" ms.\");\n");if (!"void".equals(type)) {body.append("return result;\n");}body.append("}");// replace the body of the interceptor method with generated// code block and add it to classmnew.setBody(body.toString());clas.addMethod(mnew);// print the generated code block just to show what was doneSystem.out.println("Interceptor method body:");System.out.println(body.toString());} }構(gòu)造攔截器方法的正文時(shí)使用一個(gè)?java.lang.StringBuffer?來累積正文文本(這顯示了處理?String?的構(gòu)造的正確方法,與在?StringBuilder的構(gòu)造中使用的方法是相對(duì)的)。這種變化取決于原來的方法是否有返回值。如果它?有返回值,那么構(gòu)造的代碼就將這個(gè)值保存在局部變量中,這樣在攔截器方法結(jié)束時(shí)就可以返回它。如果原來的方法類型為?void?,那么就什么也不需要保存,也不用在攔截器方法中返回任何內(nèi)容。
除了對(duì)(重命名的)原來方法的調(diào)用,實(shí)際的正文內(nèi)容看起來就像標(biāo)準(zhǔn)的 Java 代碼。它是代碼中的?body.append(nname + "($$);\n")?這一行,其中?nname?是原來方法修改后的名字。在調(diào)用中使用的?$$?標(biāo)識(shí)符是 Javassist 表示正在構(gòu)造的方法的一系列參數(shù)的方式。通過在對(duì)原來方法的調(diào)用中使用這個(gè)標(biāo)識(shí)符,在調(diào)用攔截器方法時(shí)提供的參數(shù)就可以傳遞給原來的方法。
清單 5 展示了首先運(yùn)行未修改過的?StringBuilder?程序、然后運(yùn)行?JassistTiming?程序以添加計(jì)時(shí)信息、最后運(yùn)行修改后的?StringBuilder程序的結(jié)果。可以看到修改后的?StringBuilder?運(yùn)行時(shí)會(huì)報(bào)告執(zhí)行的時(shí)間,還可以看到因?yàn)樽址畼?gòu)造代碼效率低下而導(dǎo)致的時(shí)間增加遠(yuǎn)遠(yuǎn)快于因?yàn)闃?gòu)造的字符串長度的增加而導(dǎo)致的時(shí)間增加。
清單 5. 運(yùn)行這個(gè)程序
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000 Constructed string of length 1000 Constructed string of length 2000 Constructed string of length 4000 Constructed string of length 8000 Constructed string of length 16000 [dennis]$ java -cp javassist.jar:. JassistTiming StringBuilder buildString Interceptor method body: { long start = System.currentTimeMillis(); java.lang.String result = buildString$impl($$); System.out.println("Call to method buildString took " +(System.currentTimeMillis()-start) + " ms."); return result; } Added timing to method StringBuilder.buildString [dennis]$ java StringBuilder 1000 2000 4000 8000 16000 Call to method buildString took 37 ms. Constructed string of length 1000 Call to method buildString took 59 ms. Constructed string of length 2000 Call to method buildString took 181 ms. Constructed string of length 4000 Call to method buildString took 863 ms. Constructed string of length 8000 Call to method buildString took 4154 ms. Constructed string of length 16000回頁首
可以信任源代碼嗎?
Javassist 通過讓您處理源代碼而不是實(shí)際的字節(jié)碼指令清單而使 classworking 變得容易。但是這種方便性也有一個(gè)缺點(diǎn)。正如我在?所有字節(jié)碼的源代碼中提到的,Javassist 所使用的源代碼與 Java 語言并不完全一樣。除了在代碼中識(shí)別特殊的標(biāo)識(shí)符外,Javassist 還實(shí)現(xiàn)了比 Java 語言規(guī)范所要求的更寬松的編譯時(shí)代碼檢查。因此,如果不小心,就會(huì)從源代碼中生成可能會(huì)產(chǎn)生令人感到意外的結(jié)果的字節(jié)碼。
作為一個(gè)例子,清單 6 展示了在將方法開始時(shí)的攔截器代碼所使用的局部變量的類型從?long?變?yōu)?int?時(shí)的情況。Javassist 會(huì)接受這個(gè)源代碼并將它轉(zhuǎn)換為有效的字節(jié)碼,但是得到的時(shí)間是毫無意義的。如果試著直接在 Java 程序中編譯這個(gè)賦值,您就會(huì)得到一個(gè)編譯錯(cuò)誤,因?yàn)樗`反了 Java 語言的一個(gè)規(guī)則:一個(gè)窄化的賦值需要一個(gè)類型覆蓋。
清單 6. 將一個(gè)?long?儲(chǔ)存到一個(gè)?int?中
[dennis]$ java -cp javassist.jar:. JassistTiming StringBuilder buildString Interceptor method body: { int start = System.currentTimeMillis(); java.lang.String result = buildString$impl($$); System.out.println("Call to method buildString took " +(System.currentTimeMillis()-start) + " ms."); return result; } Added timing to method StringBuilder.buildString [dennis]$ java StringBuilder 1000 2000 4000 8000 16000 Call to method buildString took 1060856922184 ms. Constructed string of length 1000 Call to method buildString took 1060856922172 ms. Constructed string of length 2000 Call to method buildString took 1060856922382 ms. Constructed string of length 4000 Call to method buildString took 1060856922809 ms. Constructed string of length 8000 Call to method buildString took 1060856926253 ms. Constructed string of length 16000取決于源代碼中的內(nèi)容,甚至可以讓 Javassist 生成無效的字節(jié)碼。清單7展示了這樣的一個(gè)例子,其中我將?JassistTiming?代碼修改為總是認(rèn)為計(jì)時(shí)的方法返回一個(gè)?int?值。Javassist 同樣會(huì)毫無問題地接受這個(gè)源代碼,但是在我試圖執(zhí)行所生成的字節(jié)碼時(shí),它不能通過驗(yàn)證。
清單7. 將一個(gè)?String?儲(chǔ)存到一個(gè)?int?中
[dennis]$ java -cp javassist.jar:. JassistTiming StringBuilder buildString Interceptor method body: { long start = System.currentTimeMillis(); int result = buildString$impl($$); System.out.println("Call to method buildString took " +(System.currentTimeMillis()-start) + " ms."); return result; } Added timing to method StringBuilder.buildString [dennis]$ java StringBuilder 1000 2000 4000 8000 16000 Exception in thread "main" java.lang.VerifyError:(class: StringBuilder, method: buildString signature:(I)Ljava/lang/String;) Expecting to find integer on stack只要對(duì)提供給 Javassist 的源代碼加以小心,這就不算是個(gè)問題。不過,重要的是要認(rèn)識(shí)到 Javassist 沒有捕獲代碼中的所有錯(cuò)誤,所以有可能會(huì)出現(xiàn)沒有預(yù)見到的錯(cuò)誤結(jié)果。
回頁首
后續(xù)內(nèi)容
Javassist 比我們?cè)诒疚闹兴懻摰膬?nèi)容要豐富得多。下一個(gè)月,我們將進(jìn)行更進(jìn)一步的分析,看一看 Javassist 為批量修改類以及為在運(yùn)行時(shí)裝載類時(shí)對(duì)類進(jìn)行動(dòng)態(tài)修改而提供的一些特殊的功能。這些功能使 Javassist 成為應(yīng)用程序中實(shí)現(xiàn)方面的一個(gè)很棒的工具,所以一定要繼續(xù)跟隨我們了解這個(gè)強(qiáng)大工具的全部內(nèi)容。
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0916/index.html
?
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/4035672.html
總結(jié)
以上是生活随笔為你收集整理的Java 编程的动态性, 第4部分: 用 Javassist 进行类转换--转载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 编程的动态性,第3部分: 应用
- 下一篇: Java 编程的动态性,第 5 部分: