嚴(yán)忠林
摘 要: 代碼動(dòng)態(tài)生成是指在程序運(yùn)行時(shí)根據(jù)實(shí)際情況即時(shí)生成需要的類代碼。它可以提高程序的靈活性,已被用于很多應(yīng)用架構(gòu)、腳本語言的實(shí)現(xiàn)中。為幫助學(xué)生掌握代碼動(dòng)態(tài)生成技術(shù),探討了相關(guān)技術(shù)的實(shí)現(xiàn)方法、工具的應(yīng)用和教學(xué)思路。
關(guān)鍵詞: 代碼動(dòng)態(tài)生成; Java虛擬機(jī); Java類文件; Bytecode; ASM
中圖分類號(hào):TP311 文獻(xiàn)標(biāo)志碼:A 文章編號(hào):1006-8228(2013)05-07-03
Using code dynamic generation to enhance the flexibility of Java programs
Yan Zhonglin
(College of information, mechanical and electrical engineering, Shanghai Normal University, Shanghai 200234, China)
Abstract: Code dynamic generation is defined as class code generated instantly according to the actual needs when the program is running. It enhances the flexibility of the program, so it has been used in many framework and scripting language implementations. Students who are familiar with Java, JVM mechanism and Java class file structure will master this technique easily. It is beneficial for them to learn new concepts and new programming models based on this technology, which help them build more efficient, flexible and innovative application projects. The implementation methods, tools and teaching considerations are discussed.
Key words: code dynamic generation; JVM; Java class files; Bytecode; ASM
0 引言
Java程序是通過JVM(Java虛擬機(jī))運(yùn)行的,JVM屏蔽了底層硬件和操作系統(tǒng)的差異,提供了一個(gè)統(tǒng)一的處理平臺(tái)。JVM根據(jù)類文件執(zhí)行運(yùn)算,類文件含有數(shù)據(jù)定義和處理代碼,是Java程序的基本表示形式。程序中各個(gè)類文件分開存儲(chǔ),運(yùn)行時(shí)按需裝載鏈接,這一點(diǎn)和C++等其他語言不同。C++在編譯時(shí)就組合所有類,形成一個(gè)完整的可運(yùn)行文件,而Java要直到運(yùn)行時(shí)才動(dòng)態(tài)組合,完成鏈接。
通常,類文件是由編譯器根據(jù)源文件自動(dòng)生成,由JVM在運(yùn)行時(shí)直接裝載的。但這不是獲得和使用它的惟一方法,某些情況下可以進(jìn)行更巧妙的處理。比如在運(yùn)行時(shí)繞過源文件直接生成需要的代碼,或者在裝載時(shí)直接修改類文件,即時(shí)改變它的行為,這就是類代碼動(dòng)態(tài)生成技術(shù)。
類代碼動(dòng)態(tài)生成技術(shù)需要直接在JVM的匯編語言——bytecode上展開工作。由于JVM模型和指令系統(tǒng)相對(duì)簡(jiǎn)單,類文件有定義明確的格式和語義,成員描述、與其他類的關(guān)聯(lián)都基于符號(hào)引用,非常易于理解和修改,這都降低了直接處理它們的難度。這種類文件分開存儲(chǔ)、按需裝載的機(jī)制,也易于在運(yùn)行時(shí)根據(jù)具體情況動(dòng)態(tài)生成、替換某個(gè)特殊代碼段。這使我們有了在運(yùn)行時(shí)改變程序行為的“魔力”,可以突破Java的某些限制,完成它本來無法實(shí)現(xiàn)的任務(wù)。
例如,作為靜態(tài)語言的Java,所有的域名、方法名都必須在編程時(shí)確定,有時(shí)這會(huì)限制程序的靈活性。雖然Java引入了“反射”機(jī)制以彌補(bǔ)此缺陷,但它的運(yùn)行效率與正常代碼相差很多,將它應(yīng)用于高頻執(zhí)行的核心部分是不可接受的,這時(shí)就希望用即時(shí)生成的、可高效執(zhí)行的代碼進(jìn)行替換。再比如,面向方面編程的實(shí)現(xiàn)需要在方法調(diào)用前后“編織”入橫切操作,這可以在編譯時(shí)進(jìn)行,但如果能在運(yùn)行時(shí)動(dòng)態(tài)地插入這些代碼,無疑更具靈活性。對(duì)象/關(guān)系映射也與此類似,需要能即時(shí)生成與關(guān)系數(shù)據(jù)庫(kù)結(jié)構(gòu)相對(duì)應(yīng)的數(shù)據(jù)對(duì)象。這些都離不開代碼動(dòng)態(tài)生成技術(shù)。
為適應(yīng)技術(shù)的發(fā)展潮流,許多學(xué)校都開設(shè)有關(guān)Java高端應(yīng)用的課程,如JavaEE,若干輕型架構(gòu),一些新型腳本語言等,它們會(huì)引入許多新概念和編程模式,如AOP、IOC、ORM等。要使學(xué)生切實(shí)領(lǐng)會(huì)和掌握這些抽象而微妙的內(nèi)容,只作表面上的介紹往往是不夠的,應(yīng)更深入地講解內(nèi)部實(shí)現(xiàn)機(jī)制,使學(xué)生知其然,也知其所以然。如果做一些核查,可以發(fā)現(xiàn)很多內(nèi)容都離不開代碼動(dòng)態(tài)生成,比較著名的就有AspectJ、Hibernate、Spring、Clojure、Groovy、JRuby、Jython、Eclipse等。由此可見,動(dòng)態(tài)代碼生成已被相當(dāng)普遍地使用了,可以認(rèn)為它是未來Java高端項(xiàng)目開發(fā)的一種基本手段。因此,對(duì)現(xiàn)在的學(xué)生適當(dāng)?shù)仄占斑@方面知識(shí)是很有必要的,這既可以使他們對(duì)Java特有的底層運(yùn)行機(jī)制有更深入的理解,又可以幫助掌握許多現(xiàn)在流行的熱門技術(shù),更重要的是為他們將來自己進(jìn)行創(chuàng)新開發(fā)打下基礎(chǔ)。
要掌握這門技術(shù),需要了解JVM的運(yùn)行機(jī)制,這看起來比較困難,但由于Java是一個(gè)經(jīng)過認(rèn)真設(shè)計(jì)、非常理想化的平臺(tái),相關(guān)內(nèi)容從總體上說還是易于理解和掌握的。經(jīng)過對(duì)講授內(nèi)容的規(guī)劃、斟酌,通常使用少量課時(shí)就能讓學(xué)生對(duì)此有較清晰的理解。接下來我們對(duì)相關(guān)的知識(shí)點(diǎn)和教學(xué)問題進(jìn)行探討。
1 了解類文件格式
代碼動(dòng)態(tài)生成技術(shù)要直接構(gòu)造可裝載執(zhí)行的類文件,因此首先必須清楚Java類文件的格式。它有非常明確的定義[1](見圖1),除了文件頭部以外,還有以下各部分。
⑴ 類型和接口部分:說明類的名字、訪問控制、父類以及所實(shí)現(xiàn)的所有接口。
⑵ 數(shù)據(jù)域池:羅列了該類本身定義的所有數(shù)據(jù)成員,說明了各自的名字、訪問控制、類型、初始值等屬性。
⑶ 方法域池:是它自身所有方法的集合,詳細(xì)描述了每個(gè)方法的名字、調(diào)用限制、參數(shù)類型、返回值、拋出異常、執(zhí)行代碼等屬性。
⑷ 類屬性池:列出類相關(guān)的屬性,如源文件名等。整個(gè)文件中還有多處可出現(xiàn)各種屬性,用于表示各類專門信息。Java已定義了20種屬性,用戶也可以引入自己需要的新屬性。其中Annotation屬性比較值得關(guān)注,它用于支持各種元編程,結(jié)合這里介紹的代碼動(dòng)態(tài)生成技術(shù),可以實(shí)現(xiàn)各種特殊功能。
⑸ 常量池:包含了所有常量,類名、域名、方法名等各種命名串,以及描述它們類型等屬性的描述串,對(duì)其他類的引用信息也在其中。常量共有14種類型,信息都用數(shù)字和字符表示,其他部分通過索引使用它們。
⑹ 方法的“code”屬性的格式:它提供對(duì)應(yīng)方法的bytecode代碼,try/catch塊位置,運(yùn)行時(shí)操作棧和局部變量區(qū)等信息。類文件格式見圖1。
[類文件格式 { /* u1 u2 u4為使用的字節(jié)數(shù)*/
類文件標(biāo)記:u4 0XCAFEBABE
JDK版本: u2 子版本號(hào); u2 主版本號(hào)
常量池: u2 常量池項(xiàng)數(shù); [常量信息]*
類型及接口:u2 訪問標(biāo)志; u2 本類索引; u2 父類索引; u2 接口項(xiàng)數(shù); [u2 接口索引]*
數(shù)據(jù)域池: u2 數(shù)據(jù)域項(xiàng)數(shù); [數(shù)據(jù)信息]*
方法域池: u2 方法域項(xiàng)數(shù); [方法信息]*
類屬性池: u2 類屬性項(xiàng)數(shù); [類屬性信息]*
}
數(shù)據(jù)信息 {
命名及描述:u2 訪問標(biāo)志; u2 數(shù)據(jù)名索引; u2 數(shù)據(jù)描述串索引;
數(shù)據(jù)屬性池:u2 數(shù)據(jù)屬性項(xiàng)數(shù); [數(shù)據(jù)屬性信息]*
}
方法信息 {
命名及描述:u2 訪問標(biāo)志; u2 方法名索引; u2 方法描述串索引;
方法屬性池:u2 方法屬性項(xiàng)數(shù); [方法屬性信息]*
}
屬性信息 { u2 屬性名索引; u4 屬性敘述長(zhǎng)度; 屬性敘述 }
常量信息 { u1 常量類型號(hào); 常量敘述 }
Code信息 {
運(yùn)行幀: u2 操作堆棧區(qū)大??; u2 局部變量區(qū)大??;
代碼塊: u4 代碼塊長(zhǎng)度; [bytecode代碼]*
異常處理池:u2 處理塊項(xiàng)數(shù); [u2 起始點(diǎn); u2終止點(diǎn); u2處理點(diǎn); u2 異常類型]*
代碼屬性池:u2 代碼屬性項(xiàng)數(shù); [代碼屬性信息]*
}]
圖1 類文件格式
類文件結(jié)構(gòu)雖然有點(diǎn)復(fù)雜,但學(xué)生只需粗略了解,使用后面介紹的ASM進(jìn)行處理,內(nèi)部細(xì)節(jié)是可以忽略的。
2 使用ASM
類文件是一個(gè)二進(jìn)制文件,要程序員直接閱讀和編寫,是很困難的,應(yīng)使用一些輔助工具軟件。目前這方面最成熟、最受歡迎的是ASM,它是一個(gè)開源軟件,相比于其他類似軟件,ASM 更小更快,也提供了更好的編程模型[3]。它還附有代碼生成工具和eclipse插件,可以在良好的人機(jī)界面下開展工作。ASM提供了兩套API,一套類似于處理XML文件的SAX,由掃描類文件產(chǎn)生的一系列事件驅(qū)動(dòng),采用訪問者模式進(jìn)行處理。另一套類似于DOM,基于掃描類文件獲得的語法樹進(jìn)行處理,兩套方法各有優(yōu)缺點(diǎn),前者速度快,所需內(nèi)存少,而后者能進(jìn)行更全面的控制。
以前套方法為例,ASM提供了類解析器ClassReader,能夠掃描解析類文件,并發(fā)出各驅(qū)動(dòng)事件;提供類生成器ClassWriter,按合適的次序調(diào)用能生成包含各種元素的二進(jìn)制類文件。ASM還有ClassVisitor、FieldVisitor、MethodVisitor、AnnotationVisitor等抽象類,為訪問類文件中各元素定義了相應(yīng)的visit方法,也按類文件的格式要求規(guī)定了調(diào)用次序。用戶只要制作子類,用自己的處理邏輯覆蓋相應(yīng)方法,無須關(guān)注字節(jié)偏移量、常量池索引號(hào)等底層細(xì)節(jié),就可實(shí)現(xiàn)從相關(guān)信息的檢測(cè)、獲取到運(yùn)行代碼的生成、修改等各種功能。
典型的代碼結(jié)構(gòu)如圖2所示,由若干ClassReader、ClassVisitor子類、ClasssWriter組成一處理鏈。每個(gè)ClassVisitor子類通過對(duì)事件的過濾、變換、轉(zhuǎn)發(fā),實(shí)現(xiàn)自己特定的功能。它可以原樣傳送事件,保持對(duì)應(yīng)元素不變;也可以改變事件參數(shù),導(dǎo)致相應(yīng)元素的修改;或者忽略某一事件不作轉(zhuǎn)發(fā),完成元素刪除;還可以引入新的事件,注入新增加的元素。復(fù)雜情況下也可將這些對(duì)象組成網(wǎng)狀結(jié)構(gòu),以完成多個(gè)類文件間的相互參照引用、合并、拆分等處理[5]。學(xué)生只需了解類文件結(jié)構(gòu),熟悉設(shè)計(jì)模式中的訪問者模式,這種修改、生成類文件的方法應(yīng)是比較容易掌握的。
[ClasssWriter cw=new ClassWriter();
ClassVisitor c1=new SubClassVisitor1(cw), c2=new SubClassVisitor2(c1);
ClassReader cr=new ClassReader(類文件).accept(c2, 0);
byte[] b=cw.toByteArray();]
圖2 處理代碼基本結(jié)構(gòu)
3 熟悉Bytecode
用ASM直接生成類文件的工作是在JVM的底層展開的,應(yīng)盡可能用于簡(jiǎn)單處理。復(fù)雜的處理還是直接使用高級(jí)語言Java較為妥當(dāng),這樣就能進(jìn)行良好的隔離、封裝,將它們組織成可組合使用的基本單元。在此基礎(chǔ)上再動(dòng)態(tài)生成一些指令,按需訪問、調(diào)用它們,完成所希望的處理。這些指令需要按照某種特定邏輯在運(yùn)行時(shí)添加、改變、刪除處理對(duì)象和運(yùn)算步驟,如果直接生成還是感覺困難,依然可以先用Java編寫一程序模版,確定處理的基本框架和各核心元素,運(yùn)行時(shí)再以此為基礎(chǔ)在指令層面上進(jìn)行簡(jiǎn)單的增刪和替換。
除了簡(jiǎn)單的類型或名字修改,以及數(shù)據(jù)或方法整體的增、刪、替換外,其他處理一般都牽涉到Bytecode代碼,因此有必要熟悉它們。JVM是一個(gè)堆棧型機(jī)器,所有的局部變量存儲(chǔ)和運(yùn)算操作都在堆棧中實(shí)現(xiàn),沒有寄存器,也不需要復(fù)雜的尋址方式,因此指令系統(tǒng)相對(duì)簡(jiǎn)單,名義上它有200多條指令[1],但經(jīng)過歸納整理,實(shí)際只有不到50條不同指令(見表1),多種變化只是根據(jù)數(shù)據(jù)類型、判斷條件而有規(guī)則添加的前后綴,完全可以舉一反三,學(xué)習(xí)起來比真實(shí)的CPU如8086等要容易得多。指令大體可以分為三類。
⑴ 堆棧操作:用于為后續(xù)操作或方法調(diào)用,在棧頂配置需要的數(shù)據(jù),或取得運(yùn)算結(jié)果。它包括局部變量、對(duì)象/類數(shù)據(jù)成員、數(shù)組元素的進(jìn)出棧,常量的進(jìn)棧,棧頂元素的復(fù)制、交換等。
⑵ 運(yùn)算操作:指定需要完成的運(yùn)算,包括算術(shù)和邏輯運(yùn)算,類型轉(zhuǎn)換等,對(duì)于對(duì)象還有構(gòu)造、判定等操作。
⑶ 流程控制:用于改變執(zhí)行流程,包括各種轉(zhuǎn)移指令、方法調(diào)用和返回指令。
這些指令恐怕是相關(guān)內(nèi)容中最繁雜的部分,教學(xué)的關(guān)鍵是要讓學(xué)生理解JVM運(yùn)行時(shí)的棧幀結(jié)構(gòu),指令也應(yīng)著重介紹動(dòng)態(tài)代碼生成時(shí)常用的部分,主要是各種方法調(diào)用,數(shù)據(jù)訪問,以及為此而必須進(jìn)行的堆棧準(zhǔn)備,表1中列出了堆棧和控制部分的相關(guān)指令。
表1 JVM指令系統(tǒng)
[堆棧\&各類變量入/出棧\&①load⑥、①store⑥、①aload、①astore、get②、put②\&常量和棧頂處理\&①const_⑥、bipush、sipush、ldc⑦、pop③、dup③、swap\&運(yùn)算\&算術(shù)運(yùn)算\&①add、①sub、①mul、①div、①rem、①neg、iinc、①cmp⑥\&位或邏輯運(yùn)算\&①shl、①shr、①ushr、①and、①or、①xor\&轉(zhuǎn)換、構(gòu)造和判定\&①2①、checkcast、new、④newarray、instanceof、arraylength\&控制\&方法調(diào)用和其他\&invoke②、monitorenter、monitorexit、nop\&條件判斷\&if⑤、if_icmp⑤、if_acmp⑤、ifnull、ifnonnull\&轉(zhuǎn)移和返回\&goto⑦、tableswitch、lookupswitch、①return、athrow、jsr⑦、ret\&①按數(shù)據(jù)類型\&②按成員屬性\&③按所需的堆棧要求\&④按創(chuàng)建的數(shù)組類型\&⑤按判斷條件\&⑥按操作要求\&⑦按數(shù)據(jù)寬度\&在此處填入相應(yīng)內(nèi)容\&]
4 掌握類裝載機(jī)制
二進(jìn)制類文件只有通過裝載機(jī)制裝入JVM才能發(fā)揮作用。通常情況下JVM只能根據(jù)classpath等預(yù)先指定的路徑搜索并載入已有文件。所以對(duì)于運(yùn)行時(shí)動(dòng)態(tài)生成的二進(jìn)制代碼,需要另行設(shè)法導(dǎo)入,才能正常使用。這就需要理解Java的類裝載機(jī)制。
Java類是在運(yùn)行時(shí)按需動(dòng)態(tài)裝載的。虛擬機(jī)中有多個(gè)類裝載器,負(fù)責(zé)獲取不同來源的類文件。除了啟動(dòng)類裝載器(Bootstrap ClassLoader),其他都繼承自抽象類ClassLoader[2]。一般情況下,多個(gè)類加載器之間采用雙親委派模型組成一裝載器樹,以保證類代碼的惟一性。類文件從裝載到可以使用要經(jīng)過載入、鏈接、初始化等階段[4],載入階段獲取需要的二進(jìn)制數(shù)據(jù)文件,鏈接階段包含了代碼合法性驗(yàn)證、存儲(chǔ)空間獲取和可選的引用類解析等操作。
因此,要能在運(yùn)行時(shí)載入動(dòng)態(tài)生成的代碼,可以通過定義自己的類裝載器完成。作為父類的ClassLoader有幾個(gè)關(guān)鍵方法:
⑴ defineClass(String name, byte[] b, int off, int len):可以將一個(gè)正確的字節(jié)數(shù)組轉(zhuǎn)換為合法的Java類。
⑵ findClass(String name):按該裝載器特定的方法獲得需要的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換為Class對(duì)象。每個(gè)自定義裝載器都應(yīng)該覆寫該方法,提供自己的載入機(jī)制。
⑶ loadClass(String name):用于裝載指定名字的類,它實(shí)現(xiàn)雙親委派模型,保證僅在必要時(shí)才調(diào)用自己的findClass方法完成裝載。通常不用改寫。
一般情況下自定義裝載器只需定義findClass方法,在其中利用ASM的ClassWrite動(dòng)態(tài)生成二進(jìn)制代碼,再通過defineClass方法將它轉(zhuǎn)化為標(biāo)準(zhǔn)的Java類。其基本結(jié)構(gòu)如圖3的上面部分所示。
[ClassLoader myClassLoader=new ClassLoader() {
public Class<?> findClass(String name) {
byte[] classfile=用ASM的ClasssWriter的toByteArray()獲得的字節(jié)數(shù)組;
return defineClass(name, classfile, 0, classfile.length);
} };
Class<?> c=myClassLoader.loadClass(name);
---------------------------------------------------------------------------------------------------------------------
public static void premain(String agentArgs, Instrumentation inst) {
ClassFileTransformer transform=new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String classname, Class<?> clazz,
ProtectionDomain domain, byte[] classfile) throws IllegalClassFormatException {
if (當(dāng)前處理的是需要變換的類代碼) {
byte[] newClassfile=用ASM對(duì)classfile變換后獲得的新數(shù)組;
return newClassfile;
} else return null;
} };
inst.addTransformer(transform,false);
}]
圖3 動(dòng)態(tài)生成代碼的裝載
在不宜使用自定義類裝載器的場(chǎng)合,也可以用java.lang.instrument包[2]完成代碼的動(dòng)態(tài)變換和生成。它有ClassFile-
Transformer和Instrumentation兩個(gè)接口,ClassFileTransformer由用戶實(shí)現(xiàn),完成代碼變換,它定義了transform方法,會(huì)在裝載器裝載了新類,對(duì)其進(jìn)行合法性驗(yàn)證之前執(zhí)行。通過覆寫該方法,用戶可以攔截新載入的類,對(duì)其進(jìn)行分類、檢測(cè)、增刪、修改,然后將變換后的新代碼交JVM執(zhí)行。使用這個(gè)包時(shí),用戶要提供一個(gè)包含premain方法的類。通過適當(dāng)?shù)拿睿瓜到y(tǒng)在main方法執(zhí)行之前執(zhí)行該方法,用JVM提供的instrumentation對(duì)象添加用戶自定義的代碼變換方法,使所有新裝載的類都經(jīng)過transform的處理,在實(shí)現(xiàn)了期望的變換后被JVM執(zhí)行。圖3的下半部分演示了這種方案。
學(xué)會(huì)這些內(nèi)容的關(guān)鍵是理解Java的類裝載過程和對(duì)應(yīng)方法,相對(duì)來說,前一方案比較容易掌握,后一方案適用于內(nèi)部已封裝使用了自定義的、非常規(guī)的類裝載器的復(fù)雜應(yīng)用架構(gòu),教學(xué)時(shí)可根據(jù)實(shí)際情況進(jìn)行選擇。
5 結(jié)束語
學(xué)習(xí)這些知識(shí),可以幫助學(xué)生了解JVM的內(nèi)部運(yùn)行機(jī)制,更好地掌握那些通過動(dòng)態(tài)代碼生成技術(shù)實(shí)現(xiàn)的新概念、新模式,使他們能透過語法層面,從本質(zhì)上深入領(lǐng)會(huì)這些內(nèi)容。學(xué)習(xí)這些知識(shí)也可使學(xué)生掌握這種能在運(yùn)行時(shí)生成、修改類代碼的編程技能。在教學(xué)中,除了集中一些課時(shí)講述以上內(nèi)容外,還可以布置一些課題,讓學(xué)生自己實(shí)踐,比如“反射”代碼的替換、動(dòng)態(tài)代理的生成、接口實(shí)現(xiàn)的按需配置等,要鼓勵(lì)學(xué)生發(fā)揮自己的想象力和創(chuàng)造性,用這種技術(shù)實(shí)現(xiàn)靜態(tài)的Java語言所無法完成的功能,培養(yǎng)起勇于開拓,善于創(chuàng)新的良好習(xí)慣。
參考文獻(xiàn):
[1] Tim Lindholm,F(xiàn)rank Yellin.The Java Virtual Machine Specification
Second Edition [M] .Addison-Wesley Professional,1999.4.
[2] Oracle co. Java Platform, Standard Edition 7 API Specification
[EB/OL].http:// docs.oracle.com/javase/7/docs/api/
[3] Eric Bruneton. ASM 4.0 A Java bytecode engineering library [EB/
OL].http:// download.forge.objectweb.org/asm/asm4-guide.pdf
[4] 周志明.深入理解Java虛擬機(jī)[M].機(jī)械工業(yè)出版社,2011.
[5] Eugene Kuleshov. Using the ASM framework to implement
common Java bytecode transformation patterns[J]. http://asm.ow2.org/current/asm-transformations.pdf