嚴忠林



摘 ?要: 學習編程離不開大量的實踐訓練,但批閱學生提交的代碼卻是一件相當費神耗時的工作。Java教學大都圍繞其功能強大的標準類庫來組織安排,并通過相應練習使學生熟練掌握。為了提高效率,設計了一個作業輔助批閱工具,它能對Java類文件進行自動修改,在運行時獲取關鍵類庫的使用信息,了解它們的調用頻次、先后次序、所用參數及返回值,可幫助理解程序邏輯,評判學生對教學內容的掌握程度。
關鍵詞: Java類文件,ASM,代碼批閱,計算機輔助教學
中圖分類號:TP399 ? ? ? ? ?文獻標識碼:A ? ? 文章編號:1006-8228(2020)01-53-04
Abstract: Coding is essential for learning a new programming language. However, for the instructors, it is very time-consuming and tedious to read over all of the code submitted by the students. The syllabus of Java programming consists of using its powerful standard libraries, accompanied with corresponding practical coding projects. A tool is designed to help the instructors to improve their code review efficiency. This tool is able to automatically modify the Java class files and return the usage of the class libraries, including their calling frequency, calling order, parameters and return values. This tool is helpful for understanding the logic of the code from students and judging their level of understanding.
Key words: Java class file; ASM; code review; CAI
0 引言
Java多年來一直是軟件開發的首選語言,也是計算機專業學習的重點,對相關知識和開發能力都有較高要求,因此需要安排充實的教學內容,進行有效的訓練實踐。對于語法已入門的學生,教學通常是圍繞著各種類庫展開的。特別是JDK提供的標準類庫,功能豐富全面、使用方便高效。從輸入輸出到集合框架、多線程、網絡、數據庫,各種功能都是通過使用相應對象,調用適當方法來實現的。即使是JavaEE、云計算等進階內容,也大都歸結為對特定類庫和框架的學習。這些內容的教學都需要有針對性地安排大量實踐,而學生的掌握程度,也要通過批閱提交的作業,查看相應API的使用情況才能知曉。因此準確全面、高效及時地處理學生作業,就變得非常重要。
閱讀他人程序,從來就不輕松。那些真正能訓練和體驗面向對象編程的作業,處理的問題和對應的程序結構都較復雜,代碼規模大,處理流程多變,運行邏輯不是一眼能看清的。況且每個學生都有自己的編程風格和習慣,審閱這些代碼相當勞心費力,如班級的人數再多一點,就非常辛苦了。過去的應對辦法,要么是不管代碼只看最后運行結果,要么是僅抽選少數幾份作業批閱。它們都難以切實了解學生的學習情況,對教學效果的評估很容易產生偏差。而且如被學生察覺這種狀況,對師生間的信任、交流、配合也是有害的。
那么如何才能既省時省力,又全面準確地檢查學生的作業情況呢?我們的建議是批閱時多關注作為教學重點的那些核心API的使用,因為作業的目的就是要通過訓練來掌握它們,它們通常也是程序處理的關鍵步驟。當然收集這些信息應該極其方便,最好是全自動的,不用在文本中搜索查找。我們設計了一個工具達到此目的,它會在程序運行中虛擬機執行類裝載時對某些指令作適當修改,讓它在完成相應操作的同時也輸出必要信息。這樣程序一運行,使用了哪些API、其順序頻度、具體數據等一目了然,從中不難推測它的設計思路,明確相關內容的掌握程度。
1 類文件結構和ASM工具
要修改Java虛擬機指令,必須對Java類文件結構有所了解。它有統一緊湊的層次格式[1],簡化后的結構如圖1所示,包含了類本身、全部的字段成員和方法成員信息,大量細節依靠相關屬性來描述,所有常數和文字信息都通過對常量池的下標引用來表達。
我們要修改的代碼在相應方法的Code屬性中,主要部分是由bytecode順序排列形成的字節數組,其中既有分支轉移及異常捕捉等復雜結構,又隱含了運行中對局部變量和操作數棧的反復使用。它還包含多種屬性,有局部變量的相關信息、程序行與代碼的對應關系、還有程序員相當陌生的程序裝載時的代碼驗證支持。它們相互聯系對照,要直接在其中修改代碼,既困難又易錯。為此我們借用了一個使用廣泛、高效方便的開源工具ASM。
ASM是Java字節碼的分析、生成、變換工具[2],它對類內成員的增刪改、對代碼內各指令的查找變換都進行了優化,隱藏了對常量池的操作,允許引入指令跳轉、劃定區域所需要的地址標號,能自動計算運行時Frame的容量、代碼驗證用的StackMapTable等。針對類文件的復雜結構,ASM采用Visitor設計模式,提供了基于事件和基于對象的兩套API。前者簡單快捷,但必須按序、即時地處理類內各元素。后者在內存中將各元素組織成樹型結構,便于根據上下文自由修改。ASM提供下列核心部件[3]。
● ClassVisitor、FieldVisitor、MethodVisitor等抽象類:定義了處理類內各種元素的visit方法簽名,用戶通過子類可實現對類內部各種成員的過濾、變換、增補等操作。它們還可接受另一Visitor對象,組成處理鏈,實現對元素的后續處理。
● ClassNode、FieldNode、MethodNode:是以上各Visitor的子類,定義了記錄類內部各種元素的數據結構和對應的visit方法,用于實現基于對象的處理方式,在變換修改后還能用accept方法發起后續處理。
● ClassReader:它有accept方法,能接受Visitor對象,并執行對指定類文件內部結構的解析,進而驅動該對象執行相應方法進行處理。
● ClassWriter:是ClassVisitor的子類,執行相應方法能構造出對應類內元素的二進制表示,最后調用toByteArray方法,即可獲得完整的類文件。
使用ASM時要將Reader、自定義的Visitor和Writer串接在一起,有時甚至形成多路結構,共同完成較復雜的處理。圖3就是對作業代碼進行修正變換所采用的結構,它會在代碼中搜尋指定的指令和結構,進行必要的修改與添加。
2 類文件的修改變換
2.1 方法調用指令及變換方案
程序中使用類庫主要是通過調用它提供的方法。Java虛擬機有5種方法調用指令,INVOKEVIRTUAL和INVOKEINTERFACE分別調用類內定義和接口定義的動態綁定方法;INVOKESPECIAL調用無需動態綁定的方法;它們都通過某個對象執行,INVOKESTATIC調用不使用對象的類方法;INVOKEDYNAMIC目前除了lambda表達式外,Java程序中并不使用。這些指令內都標明了方法的名稱、類型及所在類在常量池中的索引,因此在代碼中查找和修改特定的調用指令是比較簡單的。
Java虛擬機執行這些指令時都會在當前線程的堆棧頂創建一個Frame作為運行環境,其中包含局部變量區和操作數棧。方法調用指令前應先有指令將所需參數按序壓入操作數棧,如是對象方法,首個參數一定是隱含的this引用。調用執行時這些參數被轉錄至新的Frame,作為前幾個局部變量,在代碼中得到處理。調用完成后將撤消該Frame,如有返回結果,它會出現在原Frame的操作數棧頂。
要在使用指定的API時獲得信息,就要找到對應調用指令加以修改。比如對于正則表達式,通常關心其模式字符串的使用是否正確,這就要考察Pattern類的compile方法。最簡單的辦法是將該調用指令替換為對名為_compile的一個新方法的調用,由它再去調用原來的Pattern.compile,但額外會輸出需要的信息,比如作為參數的模式字符串等。新方法保持與原方法的參數及返回完全一致,這樣調用指令前后無需再作其他修改,只要在代碼中引入新的_compile方法就可以了。使用這種修改調用指令的方法就可方便地獲取所有要關注的API的使用信息。
除了主動調用類庫的方法外,應用程序可能還要提供某些接口或抽象類規定的實現代碼,比如Comparator接口就需要完成compare方法,它是被調用的。有時候了解它們的執行,考察其參數和返回結果,對理解整個程序也很有意義。為此可使用另一種替換方法,假如要了解compare方法的被調用情況,可將類文件中的原實現代碼改名為_compare,另生成一個替代的compare方法,它會調用被改名的_compare來實現功能,但還能輸出需要的信息,這樣就能知道比較操作的時機、次數、每次比較的值和結果等。
2.2 標注變換點
采用這種方案,必須事先聲明關注哪些方法,怎么輸出信息,為此要按圖2的樣例代碼所示定義CheckItems類。它通過一些自定義的標注指出關注的方法,@Item1和@Item2分別針對上述主動調用和被動調用的情形。它們都要寫出完整的方法名,后面再緊跟處理方法,其中除了執行原本的調用外,還說明怎么輸出信息。這些引入的代碼會按前述方案進行處理,有些要改名,會用無沖突的名字代替。但這些方法的參數和返回類型必須和原方法完全一致,由于其中所有的方法調用都改換成了類方法調用,所以如果原來是對象方法,其隱含的this引用也要作為第一個參數顯式寫出,如圖2所示。
CheckItems的父類是Check,它為處理和保存調用信息提供了一些通用方法。
● 打開并管理一輸出流,提供print()和println()等方法,用來記錄需要的信息。
● 提供fline()方法,返回調用點所在的源文件名和行號,可幫助區分同一方法在不同地點的調用,也有利于源文件閱讀時的查找。
● 如果參數或返回值是基本類型或字符串,可直接輸出,其意義很明晰。但對引用型變量t,則應使用obj(t)方法,它返回一個由類型和序號構成的、較容易理解的名稱,如Thread1、Pattern3等,還保證同一引用一定獲得同一名稱,有助于在考察流程時確定各對象前后的同一性。
● 如果對象t是自定義類型,還可用val(t)獲得其內部各字段的名字和值,比如對學生定義的日期類型,可能會輸出{year:2019,month:1,day:1},這樣可以切實了解代碼運行中的數據細節及其變化,對分析、理解程序是非常必要的。
如果要考察特定對象的生成情況,也可在其構造方法的調用指令中,進行類似地修改替換,獲得信息輸出。但構造方法在類文件中被命名為
在CheckItems類中還可根據需要引入一些狀態變量來控制信息輸出。比如對循環中的操作,有時只要查看前幾次調用或最后的調用,有時只需統計調用次數,通過引入變量就能實現這些控制。圖2的樣例代碼就統計了compare方法的調用次數,并僅對前三次輸出。由此需引入對這些狀態變量進行初始化和終極處理的begin和end方法,會在每份作業批閱的開始和結尾處執行。
對多線程相關作業,常常還關心用synchronized指示的對象鎖的狀態。該保留字在語法上有兩種用法:置于方法定義前或組成一個代碼塊,對應地在類文件中也有兩種處置[4]:在對應方法的標記中標志;用MONITORENTER/MONITOREXIT指令劃出加鎖區間,虛擬機運行時會相應的進行加/解鎖操作。批閱作業時如希望了解這些情況,可在CheckItems類中加@LockInfo標注,代碼處理時就會在合適位置插入指令,執行時就可獲得加/解鎖的時機、運行的線程、被鎖的對象等信息。
批閱學生作業當然很希望了解他們的設計,有哪幾個類、每個類的內部結構等。雖然可以查閱源代碼,但要打開不同的文件再翻閱查找,還是比較費事。為此可在CheckItems類中加@MemberInfo標注,將會在處理類文件時先輸出每個類的梗概信息,包括它們的繼承/實現關系、所有字段和方法成員的名字、類型、訪問控制等內容。這樣就大概了解了其設計結構,對理解后續運行時輸出的信息也有幫助。
2.3 代碼變換器結構
有了CheckItems文件,就明確了需要考察的方法調用以及對應的信息輸出方式,現在只需有一個代碼變換器,將這些內容“織入”到學生提交的作業中,代碼一經執行,那些關鍵步驟就被記錄下來。通過查看這些信息,就能了解學生的學習情況。
代碼變換器利用ASM提供的各種工具類構建,結構如圖3所示。其中Reader2讀入CheckItems,將其中內容存于Node對象中,并向Visitor1指出需要進行變換的方法。Reader1能讀入作業中的各個class文件,解析后交Visitor1處理,Visitor1如發現匹配的方法,將按約定修改名稱、指令, 再送至Writer,同時將需要補充的內容告訴Visitor2。Visitor2從Node中取得相應元素,作必要修改后也送至Writer。最后由Writer綜合這些輸入,構造出新的、能輸出需要信息、完整可運行的二進制類文件。
實際批閱作業時將全班的代碼組織在一個子目錄中,每人是獨立的包或jar文件,控制程序將逐個處理它們。對每份作業,生成一個自定義的類裝載器,負責裝載學生作業代碼并利用上述變換器進行必要轉換。在調用main()啟動運行后,其輸出的信息將送至指定文件。為避免處理過程脫離控制,變換器還將攔截Runtime和System 類的exit或halt調用,將其轉換為能被控制程序捕捉的異常。代碼變換器還會把代碼中所有的標準輸出也轉向至同一指定文件,這樣該程序的設計結構、運行過程中的關鍵信息和運行結果都被放置在一起,理解、評判學生作業情況就很方便了。
3 結束語
有了這一作業批閱輔助工具,可以大大提高工作效率。對于稍有規模的程序,要理解其思路,少不了要來回參照,分層拆剖。如果學生人數較多,對教師的心神精力是一個考驗。而使用本工具,因為每一關鍵步驟都有提示,比照源代碼,其運行邏輯通常立即就可以抓住。
批改作業時只閱讀程序而不運行,細節錯誤很難發現。只看運行最終結果,雖能發現有錯,但要找到問題所在也很困難。而使用本工具,只要對輸出的信息有合適安排,那么定位錯誤位置,找出癥結所在就會容易很多。
當然,在此基礎上還可以進一步開展工作,比如對處理過程、運行結果添加自動評分功能,對代碼實現簡單的數據流、控制流分析以更清晰顯現程序邏輯等,這都有待于今后繼續努力。
參考文獻(References):
[1] Tim Lindholm, Frank Yellin. The Java Virtual Machine Specification Java SE 11 Edition[M] Addison-Wesley Professional,2018.8
[2] Eric Bruneton. ASM 4.0 A Java bytecode engineering library[EB/OL].http://www.ow2.org/
[3] objectweb.org. asm 7.0 API[EB/OL]. http://www.ow2.org/
[4] James Gosling, Bill Joy. The Java LanguageSpecificationJava SE 11 Edition [M] Addison-Wesley Professional 2018.8